值类别
每个 C++ 表达式(带操作数的运算符、字面量、变量名等)都具有两个独立的属性:类型 和 值类别。每个表达式都具有某个非引用类型,并且每个表达式都属于三种主要值类别中的一个:纯右值、将亡值 和 左值。
- 计算内置运算符的操作数的值(这种纯右值没有 结果对象),或者
- 初始化一个对象(这种纯右值被称为具有 结果对象)。
扩展内容 |
---|
所以说,在历史上,左值可以出现在赋值表达式的左侧。总的来说,情况并非总是如此 void foo(); void baz() { int a; // Expression `a` is lvalue a = 4; // OK, could appear on the left-hand side of an assignment expression int &b{a}; // Expression `b` is lvalue b = 5; // OK, could appear on the left-hand side of an assignment expression const int &c{a}; // Expression `c` is lvalue c = 6; // ill-formed, assignment of read-only reference // Expression `foo` is lvalue // address may be taken by built-in address-of operator void (*p)() = &foo; foo = baz; // ill-formed, assignment of function } |
- 一个 右值 是一个纯右值或将亡值;
扩展内容 |
---|
所以说,在历史上,右值可以出现在赋值表达式的右侧。总的来说,情况并非总是如此 运行这段代码 #include <iostream> struct S { S() : m{42} {} S(int a) : m{a} {} int m; }; int main() { S s; // Expression `S{}` is prvalue // May appear on the right-hand side of an assignment expression s = S{}; std::cout << s.m << '\n'; // Expression `S{}` is prvalue // Can be used on the left-hand side too 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)}; // Expression `42` is prvalue static_assert(is_prvalue<decltype((42))>::value); // Expression `a` is lvalue static_assert(is_lvalue<decltype((a))>::value); // Expression `b` is lvalue static_assert(is_lvalue<decltype((b))>::value); // Expression `std::move(a)` is xvalue static_assert(is_xvalue<decltype((std::move(a)))>::value); // Type of variable `r` is rvalue reference static_assert(std::is_rvalue_reference<decltype(r)>::value); // Type of variable `b` is lvalue reference static_assert(std::is_lvalue_reference<decltype(b)>::value); // Expression `r` is lvalue static_assert(is_lvalue<decltype((r))>::value); } |
内容 |
[编辑] 主要类别
[编辑] 左值
以下是左值表达式
- 变量名称,函数,模板参数对象(自 C++20 起),或数据成员,无论类型如何,例如 std::cin 或 std::endl。即使变量的类型是右值引用,由其名称组成的表达式也是一个左值表达式(但请参见 可移动表达式);
扩展内容 |
---|
void foo() {} void baz() { // `foo` is lvalue // address may be taken by built-in address-of operator void (*p)() = &foo; } struct foo {}; template <foo a> void baz() { const foo* obj = &a; // `a` is an lvalue, template parameter object } |
- 函数调用或重载运算符表达式,其返回值类型是左值引用,例如 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()` is lvalue, function call whose return type is lvalue reference } |
- a = b,a += b,a %= b,以及所有其他内置的 赋值和复合赋值 表达式;
- ++a 和 --a,内置的 前缀自增和自减 表达式;
- *p,内置的 间接访问 表达式;
- a[n] 和 p[n],内置的 下标 表达式,其中 a[n] 中的一个操作数是数组左值(自 C++11 起);
- a.m,对象成员 表达式,除了
m
是成员枚举器或非静态成员函数,或者 a 是右值且m
是对象类型的非静态数据成员;
扩展内容 |
---|
struct foo { enum bar { m // member enumerator }; }; void baz() { foo a; a.m = 42; // ill-formed, lvalue required as left operand of assignment } struct foo { void m() {} // non-static member function }; void baz() { foo a; // `a.m` is a prvalue, hence the address cannot be taken by built-in // address-of operator void (foo::*p1)() = &a.m; // ill-formed void (foo::*p2)() = &foo::m; // OK: pointer to member function } struct foo { static void m() {} // static member function }; void baz() { foo a; void (*p1)() = &a.m; // `a.m` is an lvalue void (*p2)() = &foo::m; // the same } |
- p->m,内置的 指针成员 表达式,除了
m
是成员枚举器或非静态成员函数; - a.*mp,对象成员指针 表达式,其中 a 是左值且
mp
是数据成员指针; - p->*mp,内置的 指针成员指针 表达式,其中
mp
是数据成员指针; - a, b,内置的 逗号 表达式,其中 b 是左值;
- a ? b : c,三元条件 表达式,用于某些 b 和 c(例如,当两者都是相同类型的左值时,但请参见 定义 获取详细信息);
- 一个 字符串字面量,例如 "Hello, world!";
- 一个转换为左值引用类型的强制转换表达式,例如 static_cast<int&>(x) 或 static_cast<void(&)(int)>(x);
- 一个非类型 模板参数,其类型为左值引用;
template <int& v> void set() { v = 5; // template parameter is lvalue } int a{3}; // static variable, fixed address is known at compile-time void foo() { set<a>(); }
|
(自 C++11 起) |
属性
- 与 纯左值(在下文)相同。
- 左值的地址可以使用内置的地址运算符获取:&++i[1] 和 &std::endl 是有效的表达式。
- 可修改的左值可以用作内置的赋值运算符和复合赋值运算符的左侧操作数。
- 左值可用于 初始化左值引用;这会将一个新名称与表达式标识的对象相关联。
[编辑] 纯右值
以下是纯右值表达式
- 一个 字面量(除了 字符串字面量),例如 42,true 或 nullptr;
- 函数调用或重载运算符表达式,其返回值类型是非引用类型,例如 str.substr(1, 2),str1 + str2,或 it++;
- a++ 和 a--,内置的 后缀自增和自减 表达式;
- a + b,a % b,a & b,a << b,以及所有其他内置的 算术 表达式;
- a && b,a || b,!a,内置的 逻辑 表达式;
- a < b,a == b,a >= b,以及所有其他内置的 比较 表达式;
- &a,内置的 地址运算符 表达式;
- a.m,对象成员 表达式,其中
m
是成员枚举器或非静态成员函数[2]; - p->m,内置的 指针成员 表达式,其中
m
是成员枚举器或非静态成员函数[2]; - a.*mp,对象成员指针 表达式,其中
mp
是指向成员函数的指针[2]; - p->*mp,内置的 指针成员指针 表达式,其中
mp
是指向成员函数的指针[2]; - a, b,内置的 逗号 表达式,其中 b 是纯右值;
- a ? b : c,三元条件 表达式,用于某些 b 和 c(请参见 定义 获取详细信息);
- 对非引用类型的强制转换表达式,例如 static_cast<double>(x)、std::string{} 或 (int)42。
this
指针;- 一个 枚举器;
- 标量类型的非类型 模板参数;
template <int v> void foo() { // not an lvalue, `v` is a template parameter of scalar type int const int* a = &v; // ill-formed v = 3; // ill-formed: lvalue required as left operand of assignment }
|
(自 C++11 起) |
|
(自 C++20 起) |
属性
- 与 右值 (下文) 相同。
- 纯右值不能是 多态 的:它所表示的对象的 动态类型 始终是表达式的类型。
- 非类非数组纯右值不能是 cv 限定 的,除非为了绑定到 cv 限定类型的引用而被 具体化(自 C++17 起)。(注意:函数调用或强制转换表达式可能会导致非类 cv 限定类型的纯右值,但 cv 限定符通常会立即被剥离。)
- 纯右值不能具有 不完整类型 (除了类型 void,见下文,或在
decltype
说明符中使用)。 - 纯右值不能具有 抽象类类型 或其数组。
[edit] 将亡值
以下表达式是将亡值表达式
- a.m,对象成员 表达式,其中 a 是一个右值,
m
是对象类型的非静态数据成员; - a.*mp, 指向对象成员的指针 表达式,其中 a 是一个右值,
mp
是指向数据成员的指针; - a, b,内置的 逗号 表达式,其中 b 是一个将亡值;
- a ? b : c,三元条件 表达式,用于某些 b 和 c(请参见 定义 获取详细信息);
|
(自 C++11 起) |
|
(自 C++17 起) |
|
(自 C++23 起) |
属性
- 与右值 (下文) 相同。
- 与左值 (下文) 相同。
特别是,与所有右值一样,将亡值绑定到右值引用;与所有左值一样,将亡值可能是 多态 的,非类将亡值可能是 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); // Function call with return type rvalue reference is xvalue static_assert(is_xvalue<decltype( (f()) )>::value); // Member of object expression, object is xvalue, `m` is a non-static data member static_assert(is_xvalue<decltype( (f().m) )>::value); // A cast expression to rvalue reference static_assert(is_xvalue<decltype( (static_cast<A&&>(a)) )>::value); // Operator expression, whose return type is rvalue reference to object static_assert(is_xvalue<decltype( (a + a) )>::value); // Expression `ar` is lvalue, `&ar` is valid static_assert(is_lvalue<decltype( (ar) )>::value); [[maybe_unused]] A* ap = &ar; } |
[edit] 混合类别
[edit] 左值
一个左值表达式要么是左值,要么是将亡值。
属性
[edit] 右值
一个右值表达式要么是纯右值,要么是将亡值。
属性
- 不能通过内置的取地址运算符获取右值的地址:&int()、&i++[3]、&42 和 &std::move(x) 无效。
- 右值不能用作内置赋值运算符或复合赋值运算符的左侧操作数。
- 右值可以用于 初始化 const 左值引用,在这种情况下,右值标识的临时对象的生存期将 延长,直到引用的作用域结束。
(自 C++11 起) |
[edit] 特殊类别
[edit] 待定成员函数调用
表达式 a.mf 和 p->mf, 其中 mf
是一个 非静态成员函数,以及表达式 a.*pmf 和 p->*pmf, 其中 pmf
是一个 指向成员函数的指针,被归类为纯右值表达式,但它们不能用于初始化引用、作为函数参数,或用于任何其他目的,除了作为函数调用运算符的左侧参数,例如 (p->*pmf)(args)。
[edit] 空表达式
返回 void 的函数调用表达式、强制转换为 void 的强制转换表达式和 抛出表达式 被归类为纯右值表达式,但它们不能用于初始化引用或作为函数参数。它们可以在丢弃值上下文中使用(例如,单独一行,作为逗号运算符的左侧操作数,等等),以及在返回 void 的函数的 return 语句中。此外,抛出表达式可以用作 条件运算符 ?: 的第二个和第三个操作数。
空表达式没有结果对象。 |
(自 C++17 起) |
[edit] 位域
指定 位域 的表达式(例如,a.m,其中 a 是类型 struct A { int m: 3; } 的左值)是一个左值表达式:它可以用作赋值运算符的左侧操作数,但不能获取其地址,也不能将非 const 左值引用绑定到它。const 左值引用或右值引用可以从位域左值初始化,但将创建一个位域的临时副本:它不会直接绑定到位域。
可移动表达式尽管由任何变量的名称组成的表达式都是一个左值表达式,但如果该表达式作为以下运算符的操作数出现,则该表达式可能是可移动的 如果一个表达式是可移动的,它被视为 一个右值或一个左值(直到 C++23)一个右值(自 C++23 起) 用于 重载解析(因此它可以选择 移动构造函数)。有关详细信息,请参阅 自动从局部变量和参数移动。 |
(自 C++11 起) |
[编辑] 历史
[编辑] CPL
编程语言 CPL 首次引入了表达式的值类别:所有 CPL 表达式都可以以“右手模式”进行评估,但只有某些类型的表达式在“左手模式”下有意义。在右手模式下进行评估时,表达式被视为计算值的规则(右手值,或右值)。在左手模式下进行评估时,表达式实际上给出了一个地址(左手值,或左值)。这里的“左”和“右”分别代表“赋值的左侧”和“赋值的右侧”。
[编辑] C
C 编程语言遵循类似的分类法,除了赋值的作用不再重要:C 表达式在“左值表达式”和其他表达式(函数和非对象值)之间进行分类,其中“左值”表示标识对象的表达式,一个“定位器值”[4]。
[编辑] C++98
2011 年之前的 C++ 遵循 C 模型,但恢复了“右值”这个名称用于非左值表达式,将函数变成了左值,并添加了规则,即引用可以绑定到左值,但只有对 const 的引用可以绑定到右值。一些非左值 C 表达式在 C++ 中变成了左值表达式。
[编辑] C++11
随着 C++11 中移动语义的引入,值类别被重新定义以表征表达式的两个独立属性[5]
- 具有标识:可以确定表达式是否引用与另一个表达式相同的实体,例如通过比较它们标识的对象或函数的地址(直接或间接获得);
- 可以从其移动:移动构造函数、移动赋值运算符或实现移动语义的其他函数重载可以绑定到表达式。
在 C++11 中,满足以下条件的表达式:
- 具有标识且无法从其移动的称为左值表达式;
- 具有标识且可以从其移动的称为x 值表达式;
- 不具有标识且可以从其移动的称为纯右值(“pure rvalue”)表达式;
- 不具有标识且无法从其移动的不会被使用[6]。
具有标识的表达式称为“广义左值表达式”(glvalue 代表“广义左值”。左值和 x 值都是广义左值表达式。
可以从其移动的表达式称为“右值表达式”。纯右值和 x 值都是右值表达式。
[编辑] C++17
在 C++17 中,复制省略 在某些情况下是强制性的,这需要将纯右值表达式与其初始化的临时对象分离,从而产生了我们今天所拥有的系统。请注意,与 C++11 方案相比,纯右值不再从其移动。
[编辑] 脚注
- ↑ 假设 i 具有内置类型,或者前置递增运算符被 重载 以返回左值引用。
- ↑ 2.0 2.1 2.2 2.3 特殊右值类别,请参阅 挂起成员函数调用。
- ↑ 假设 i 具有内置类型,或者后置递增运算符未被 重载 以返回左值引用。
- ↑ “C 社区内部对左值的含义存在分歧,一派认为左值是任何类型的对象定位器,另一派认为左值在赋值运算符的左侧是有意义的。C89 委员会采用了左值作为对象定位器的定义。”—— ANSI C Rationale, 6.3.2.1/10。
- ↑ "New" Value Terminology by Bjarne Stroustrup, 2010.
- ↑ const 纯右值(仅允许用于类类型)和 const x 值不会绑定到
T&&
重载,但它们会绑定到 const T&& 重载,它们也被标准归类为“移动构造函数”和“移动赋值运算符”,满足用于此分类的“可以从其移动”的定义。但是,此类重载无法修改其参数,并且实际上不会被使用;在没有这些重载的情况下,const 纯右值和 const x 值会绑定到 const T& 重载。
[编辑] 参考文献
- C++23 标准 (ISO/IEC 14882:2024)
- 7.2.1 值类别 [basic.lval]
- C++20 标准 (ISO/IEC 14882:2020)
- 7.2.1 值类别 [basic.lval]
- C++17 标准 (ISO/IEC 14882:2017)
- 6.10 左值和右值 [basic.lval]
- C++14 标准 (ISO/IEC 14882:2014)
- 3.10 左值和右值 [basic.lval]
- C++11 标准 (ISO/IEC 14882:2011)
- 3.10 左值和右值 [basic.lval]
- C++98 标准 (ISO/IEC 14882:1998)
- 3.10 左值和右值 [basic.lval]
[编辑] 缺陷报告
以下更改行为的缺陷报告被追溯应用于先前发布的 C++ 标准。
DR | 应用于 | 发布时的行为 | 正确的行为 |
---|---|---|---|
CWG 616 | C++11 | 成员访问和通过 指向成员的指针对右值的访问导致纯右值 |
重新分类为 x 值 |
CWG 1059 | C++11 | 数组纯右值无法被 cv 限定 | 允许 |
CWG 1213 | C++11 | 对数组右值的索引导致左值 | 重新分类为 x 值 |
[编辑] 另请参阅
C 文档 for 值类别
|
[编辑] 外部链接
1. | C++ 值类别和 decltype 的解惑 — David Mazières, 2021 | |
2. | 经验性地确定表达式的值类别 — StackOverflow |