C++ 值的分类(Value Categories)
目录
1 概述
2 主要分类
1.1 左值(lvalue)
1.1.1 左值详情
1.1.2 左值属性
1.2 纯右值(prvalue)
1.2.1 纯右值详情
1.2.2 纯右值属性
1.3 将逝值(xvalue)
1.3.1 将逝值详情
1.3.2 将逝值属性
3 混合分类
3.1 泛型左值(glvalue)
3.1.1 泛型左值详情
3.1.2 泛型左值属性
3.2 右值(rvalue)
3.2.1 右值详情
3.2.2 右值属性
1 概述
每一个C++ 表示达(带操作数的操作符、文字量、变量名、等等)都由两个独立属性刻画:类型(type)及类型值所属分类(type value category),值类别是编译器在表达式求值期间创建、复制和移动临时对象时必须遵循的规则的基础。每一个表达式都有一些非引用表达式,且每一个表达式都恰好归属于三种主要的值分类之一:纯右值(prvalue),将逝值(xvalue)和左值(lvalue)。这些分类的关系如下图:
主次分类为:
(1) 泛型左值(glvalue——“generalized” lvalue): 是一个表达式,其求值(evaluation)决定了对象,位域,或函数的身份。
(2) 纯右值(prvalue——“pure”rvalue):是一个表达式,其求值
a. 计算内置运算符的操作数的值(此类 prvalue 没有结果对象),或
b. 初始化一个对象(这种prvalue被认为有一个结果对象)。
结果对象可以是变量、由 new 表达式创建的对象、由临时实现创建的临时对象或其成员。请注意,非 void 废弃值表达式具有结果对象(实现的临时对象)。此外,每个类和数组 prvalue 都有一个结果对象,除非它是 decltype 的操作数;
(3) 将逝值(xvalue——“eXpiring”value,即将消亡(或正在消亡)的值): 是一个泛型左值,表示一个对象的资源可以重用,将逝值表达式具有一个地址,该地址不供程序访问,但可用于初始化右值引用,从而提供对表达式的访问。示例包括返回右值引用的函数调用,以及数组下标、成员和指向成员的指针表达式,其中数组或对象是右值引用。;
(4) 左值(lvalue):左值是非将逝值的泛型左值,其具有一个程序可访问的地址(注:左值就是可通过直接取其内存地址进行操作的对象,而右值没有可供程序访问的地址)。从历史上看,之所以这样称呼,是因为左值可以出现在赋值表达式的左侧。一般来说,情况并非总是如此:
void foo();
void baz()
{
int a; // 表达式`a` 是一个左值,因为可通过取其内存进行直接访问
a = 4; // 正确,可以出现在表达式左侧
int &b{a}; // 表达式 `b` 是一个左值
b = 5; //正确,可以出现在表达式左侧
const int &c{a}; // 表达式 `c` 是一个左值,因为可通过取其内存进行直接访问
c = 6; // 错误, 只读引用不能赋值
// 表达式 `foo` 是一个左值,类为函数也可以通过取其地址进行直接访问
// 可通过内置取地址运算符取其地址
void (*p)() = &foo;
foo = baz; // 错误, 不能将函数赋值给函数地址
}
(5) 右值(rvalue):是纯粹的右值或将逝值。从历史上看,之所以这样称呼,是因为右值可以出现在赋值表达式的右侧。一般来说,情况也并非总是如此:
#include <iostream>
struct S
{
S() : m{42} {}
S(int a) : m{a} {}
int m;
};
int main()
{
S s;
// 表达式 `S{}` 是一个纯粹右值
// 可以出现在赋值表达式的右侧
s = S{}; // 0初始化
std::cout << s.m << '\n';
// 表达式 `S{}` 是一个纯粹的右值
// 也可以用在表达式的左边
std::cout << (S{} = S{7}).m << '\n';
}
输出:
42
7
注意:此分类法在过去的 C++ 标准修订中经历了重大变化,详情请参阅下面的历史记录。
尽管这些术语有这样的名字,但它们是对表达式进行分类,而不是对值进行分类。
#include <type_traits>
#include <utility>
template <class T> struct is_prvalue : std::true_type {};
template <class T> struct is_prvalue<T&> : std::false_type {};
template <class T> struct is_prvalue<T&&> : std::false_type {};
template <class T> struct is_lvalue : std::false_type {};
template <class T> struct is_lvalue<T&> : std::true_type {};
template <class T> struct is_lvalue<T&&> : std::false_type {};
template <class T> struct is_xvalue : std::false_type {};
template <class T> struct is_xvalue<T&> : std::false_type {};
template <class T> struct is_xvalue<T&&> : std::true_type {};
int main()
{
int a{42};
int& b{a};
int&& r{std::move(a)};
// 表达式 `42` 是纯右值
static_assert(is_prvalue<decltype((42))>::value);
//表达式 `a` 是一个左值
static_assert(is_lvalue<decltype((a))>::value);
//表达式`b`是一个左值
static_assert(is_lvalue<decltype((b))>::value);
//表达式`std::move(a)` 是一个将逝值
static_assert(is_xvalue<decltype((std::move(a)))>::value);
// 变量类型 `r` 是一个右值引用
static_assert(std::is_rvalue_reference<decltype(r)>::value);
//变量类型`b` 是一个左值引用
static_assert(std::is_lvalue_reference<decltype(b)>::value);
//表达式`r`是左值
static_assert(is_lvalue<decltype((r))>::value);
}
2 主要分类
1.1 左值(lvalue)
1.1.1 左值详情
下面的表达式是左值表达式:
(1) 变量、函数、模板参数对象(自 C++20 起)或数据成员的名称,无论类型如何,例如 std::cin 或 std::endl。即使变量的类型是右值引用,由其名称组成的表达式也是左值表达式(但请参阅可移动表达式)。
void foo() {}
void baz()
{
// `foo` 是一个左值
// 可通过直接取其内存地址进行访问
void (*p)() = &foo;
}
struct foo {};
template <foo a>
void baz()
{
const foo* obj = &a; // `a` 是一个左值, 模板参数对象
}
(2) 函数调用或重载运算符表达式,其返回类型为左值引用,例如 std::getline(std::cin, str)、std::cout << 1、str1 = str2 或 ++it。
int& a_ref()
{
static int a{3};
return a;
}
void foo()
{
a_ref() = 5; // `a_ref()` 是一个左值, 函数调用其返回类型是一个左值引用
}
(3) a = b、a += b、a %= b 以及所有其他内置赋值和复合赋值是左值表达式。
(4) ++a 和 --a,内置的前增量和前减量表达式是左值表达式。
(5) *p,内置间接左值表达式。
(6) a[n] 和 p[n],内置下标表达式,其中 a[n] 中的一个操作数是数组左值(自 C++11 起)。
(7) a.m,对象表达式的成员,除非 m 是成员枚举器或非静态成员函数,或者 a 是右值且 m 是对象类型的非静态数据成员。
struct foo
{
enum bar
{
m //成员枚举器
};
};
void baz()
{
foo a;
a.m = 42; // 错, 需要左值作为赋值的左操作数
}
struct foo
{
void m() {} // 非静态成员函数
};
void baz()
{
foo a;
// `a.m` 是一个纯右值, 因此不能通过内置取地址运算符取得其地址
void (foo::*p1)() = &a.m; // 错
void (foo::*p2)() = &foo::m; // OK: 成员函数指针
}
(8) p->m,指针表达式的内置成员,除非 m 是成员枚举器或非静态成员函数。
(9) a.*mp,指向对象表达式的成员的指针,其中 a 是左值,mp 是指向数据成员的指针。
(10) p->*mp,指针表达式的内置指向成员的指针,其中mp是指向数据成员的指针。
(11) a,b,内置逗号表达式,其中b是左值。
(12) a ? b : c,对于特定的 b 和 c 的三元条件表达式(例如,当两者都是同一类型的左值时,但请参阅定义以了解详细信息)。
(13) 字符串文字量,例如 "Hello, world!";
(14)转换为左值引用类型的转换表达式,例如 static_cast<int&>(x) 或 static_cast<void(&)(int)>(x);
(15) 左值引用类型的非类型模板参数;
template <int& v>
void set()
{
v = 5; // 模板参数是左值
}
int a{3}; //静态变量, 固定地址在编译时已知
void foo()
{
set<a>();
}
(16) 函数调用或重载运算符表达式,其返回类型是函数的右值引用(C++11及以上版本)。
(17) 强制转换表达式为函数类型的右值引用,例如 static_cast<void(&&)(int)>(x) (C++11及以上版本)。
1.1.2 左值属性
(1) 与泛型左值相同(见下文)。
(2) 左值的地址可由内置地址运算符获取:&++i[1] 和 &std::endl 是有效表达式。
(3) 可修改的左值可用作内置赋值和复合赋值运算符的左侧操作数。
(4) 左值可用于初始化左值引用;这会将新名称与表达式标识的对象关联起来。
1.2 纯右值(prvalue)
1.2.1 纯右值详情
下面的表达式是纯右值表达式:
(1) 字面量(字符串字面量除外),例如 42、true 或 nullptr。
(2) 函数调用或重载运算符表达式,其返回类型为非引用,例如 str.substr(1, 2)、str1 + str2 或 it++。
(3) a++ 和 a--,内置后增和后减表达式。
(4) a + b、a % b、a & b、a << b 和所有其他内置算术表达式。
(5) a && b、a || b、!a,内置逻辑表达式。
(6) a < b、a == b、a >= b 和所有其他内置比较表达式。
(7) &a,内置地址表达式。
(8) a.m,对象成员表达式,其中 m 是成员枚举器或非静态成员函数[2] 。
(9) p->m,指针表达式的内置成员,其中 m 是成员枚举器或非静态成员函数[2] 。
(10) a.*mp,指向对象成员的指针表达式,其中 mp 是指向成员函数的指针[2] 。
(11) p->*mp,指向指针表达式的内置成员的指针,其中 mp 是指向成员函数的指针[2] 。
(12) a, b,内置逗号表达式,其中 b 是纯右值。
(12) a ? b : c,针对特定 b 和 c 的三元条件表达式(详情请参阅定义)。
(13) 转换为非引用类型的强制转换表达式,例如 static_cast<double>(x)、std::string{} 或 (int)42。
(14) this 指针。
(15) 枚举器。
(16) 标量类型的非类型模板参数。
template <int v>
void foo()
{
// 非左值, `v` 是一个类型为int标量参数模板
const int* a = &v; // 错
v = 3; // 错: 需要左值作为赋值的左操作数
}
(17) lambda 表达式,例如 [](int x){ return x * x; }(C++11及以上版本)。
(18) 需要表达式,例如需要 (T i) { typename T::type; }(C++11及以上版本)。
(19) 概念的特化,例如 std::equality_comparable<int>(C++11及以上版本)。
1.2.2 纯右值属性
(1) 与右值相同(见下文)。
(2) 纯右值不能是多态的:它表示的对象的动态类型始终是表达式的类型。
(3) 非类非数组右值不能是 cv修饰的的,除非它被具体化以绑定到对 cv 修饰类型的引用(C++17及以上版本)。(注意:函数调用或强制转换表达式可能导致非类 cv 修饰类型的 右值,但 cv 修饰符通常会立即被删除。)
(4) 右值不能具有不完整类型(void 类型除外,见下文,或在 decltype 说明符中使用时)。
(5) 右值不能具有抽象类类型或其数组。
1.3 将逝值(xvalue)
1.3.1 将逝值详情
下面的表达式是将逝值表达式:
(1) a.m,对象成员表达式,其中 a 是右值,m 是对象类型的非静态数据成员。
(2) a.*mp,对象成员指针表达式,其中 a 是右值,mp 是数据成员指针。
(3) a, b,内置逗号表达式,其中 b 是 将逝值。
(4) a ? b : c,针对特定 b 和 c 的三元条件表达式(详情请参阅定义)。
(5) 函数调用或重载运算符表达式,其返回类型为对象的右值引用,例如 std::move(x)(C++11及以上版本)。
(6) a[n],内置下标表达式,其中一个操作数是数组右值(C++11及以上版本)。
(7) 强制转换为对象类型的右值引用的表达式,例如 static_cast<char&&>(x) (C++11及以上版本)。
(8) 任何指定临时对象的表达式,在临时实现之后(C++17及以上版本)。
(9) 可移动的表达式。(C++23及以上版本)。
1.3.2 将逝值属性
(1) 同右值(下述)。
(2) 同泛左值(下述)。
具体来说,与所有右值一样,将逝值绑定到右值引用,并且与所有泛左值一样,将逝值可以是多态的,并且非类 将逝值可以是 cv 限定的。
#include <type_traits>
template <class T> struct is_prvalue : std::true_type {};
template <class T> struct is_prvalue<T&> : std::false_type {};
template <class T> struct is_prvalue<T&&> : std::false_type {};
template <class T> struct is_lvalue : std::false_type {};
template <class T> struct is_lvalue<T&> : std::true_type {};
template <class T> struct is_lvalue<T&&> : std::false_type {};
template <class T> struct is_xvalue : std::false_type {};
template <class T> struct is_xvalue<T&> : std::false_type {};
template <class T> struct is_xvalue<T&&> : std::true_type {};
// Example from C++23 standard: 7.2.1 Value category [basic.lval]
struct A
{
int m;
};
A&& operator+(A, A);
A&& f();
int main()
{
A a;
A&& ar = static_cast<A&&>(a);
// 具有返回类型左值引用的函数调用是将逝值 static_assert(is_xvalue<decltype( (f()) )>::value);
// 对象表达成员, 对象是将逝值, `m` 是非静态数据成员
static_assert(is_xvalue<decltype( (f().m) )>::value);
// 右值引用转换表达式
static_assert(is_xvalue<decltype( (static_cast<A&&>(a)) )>::value);
// 运算符表达式,其返回类型是对象的右值引用
static_assert(is_xvalue<decltype( (a + a) )>::value);
// 表达式`ar`左值, `&ar` 有效
static_assert(is_lvalue<decltype( (ar) )>::value);
[[maybe_unused]] A* ap = &ar;
}
3 混合分类
3.1 泛型左值(glvalue)
3.1.1 泛型左值详情
泛型左值表达式要么是左值,要么是将逝值。
3.1.2 泛型左值属性
(1) 泛左值可以通过左值到右值、数组到指针或函数到指针的隐式转换隐式转换为纯右值。
(2) 泛左值可以是多态的:它所标识的对象的动态类型不一定是表达式的静态类型。
(3) 泛左值可以具有不完整类型,只要表达式允许。
3.2 右值(rvalue)
3.2.1 右值详情
右值表达式要么是纯右值,要么是将逝值。
3.2.2 右值属性
(1) 右值的地址不能由内置地址运算符获取:&int(),&i++[3],&42 和 &std::move(x) 等操作无效。
(2) 右值不能用作内置赋值或复合赋值运算符的左侧操作数。
(3) 右值可用于初始化 const 左值引用,在这种情况下,右值标识的临时对象的生存期将延长,直到引用的范围结束。
(4) 右值可用于初始化右值引用,在这种情况下,右值标识的临时对象的生存期将延长,直到引用的范围结束(C++11 及以上版本)。
(5) 当用作函数参数并且该函数有两个重载可用时,一个采用右值引用参数,另一个采用对 const 参数的左值引用,则右值绑定到右值引用重载(因此,如果复制和移动构造函数都可用,则右值参数会调用移动构造函数,复制和移动赋值运算符也是如此) (C++11 及以上版本)。