值类别
每个 C++ 表达式(运算符及其操作数、字面量、变量名等)都由两个独立的属性来描述:一个类型和一个值类别。每个表达式都有一个非引用类型,并且每个表达式都精确地属于三种主要值类别之一:prvalue、xvalue 和 lvalue。
- 计算内置运算符操作数的值(这种 prvalue 没有结果对象),或者
- 初始化一个对象(这种 prvalue 被认为有一个结果对象)。
- 结果对象可以是一个变量,一个由 new-expression 创建的对象,一个由 临时实质化 创建的临时对象,或者它们的成员。请注意,非 void 被丢弃的 表达式具有结果对象(实质化的临时对象)。此外,每个类和数组 prvalue 都有一个结果对象,除非它是
decltype
的操作数;
扩展内容 |
---|
历史地,被称为左值,因为左值可以出现在赋值表达式的左侧。通常情况并非总是如此 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 } |
- 一个 rvalue 是一个 prvalue 或一个 xvalue;
扩展内容 |
---|
历史地,被称为右值,因为右值可以出现在赋值表达式的右侧。通常情况并非总是如此 运行此代码 #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); } |
目录 |
[编辑] 主要类别
[编辑] lvalue
以下表达式是lvalue 表达式
- 变量、函数、非类型模板参数对象(C++20 起)或数据成员的名称,无论类型如何,例如 std::cin 或 std::endl。即使变量的类型是右值引用,由其名称组成的表达式也是一个 lvalue 表达式(但请参阅可移动表达式);
扩展内容 |
---|
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 起) |
属性
- 同glvalue(下述)。
- 可以通过内置的取地址运算符获取左值的地址:&++i[1] 和 &std::endl 是有效表达式。
- 可修改的左值可以用作内置赋值和复合赋值运算符的左操作数。
- 左值可用于初始化左值引用;这为表达式标识的对象关联一个新名称。
[编辑] prvalue
以下表达式是 prvalue 表达式
- 字面量(字符串字面量除外),例如 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 是 prvalue;
- 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 起) |
属性
- 同rvalue(下述)。
- prvalue 不能是多态的:它所表示的对象的动态类型始终是表达式的类型。
- 非类非数组 prvalue 不能是 cv 限定的,除非它为了绑定到 cv 限定类型的引用而被实质化(C++17 起)。(注意:函数调用或类型转换表达式可能导致非类 cv 限定类型的 prvalue,但 cv 限定符通常会立即被剥离。)
- prvalue 不能具有不完整类型(除了类型 void,见下文,或在
decltype
说明符中使用时)。 - prvalue 不能具有抽象类类型或其数组。
[编辑] xvalue
以下表达式是 xvalue 表达式
- a.m,对象的成员表达式,其中 a 是右值,
m
是对象类型的非静态数据成员; - a.*mp,指向对象成员的指针表达式,其中 a 是右值,
mp
是指向数据成员的指针; - a, b,内置的逗号表达式,其中 b 是 xvalue;
- a ? b : c,某些 b 和 c 的三元条件表达式(请参阅定义了解详情);
|
(C++11 起) |
|
(C++17 起) |
(C++23 起) |
属性
- 同 rvalue(下述)。
- 同 glvalue(下述)。
特别地,像所有右值一样,xvalue 绑定到右值引用,并且像所有 glvalue 一样,xvalue 可以是多态的,非类 xvalue 可以是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; } |
[编辑] 混合类别
[编辑] glvalue
一个 glvalue 表达式 既可以是 lvalue 也可以是 xvalue。
属性
- glvalue 可以通过左值到右值、数组到指针或函数到指针的隐式转换隐式转换为 prvalue。
- glvalue 可以是多态的:它所标识的对象的动态类型不一定是表达式的静态类型。
- glvalue 可以具有不完整类型,只要表达式允许。
[编辑] rvalue
一个 rvalue 表达式 既可以是 prvalue 也可以是 xvalue。
属性
- 右值的地址不能通过内置的取地址运算符获取:&int()、&i++[3]、&42 和 &std::move(x) 是无效的。
- 右值不能用作内置赋值或复合赋值运算符的左操作数。
- 右值可用于初始化 const 左值引用,在这种情况下,右值标识的临时对象的生命周期将延长至引用作用域结束。
(C++11 起) |
[编辑] 特殊类别
[编辑] 待决成员函数调用
表达式 a.mf 和 p->mf,其中 mf
是非静态成员函数,以及表达式 a.*pmf 和 p->*pmf,其中 pmf
是指向成员函数的指针,都被归类为 prvalue 表达式,但它们不能用于初始化引用、作为函数参数,或用于任何目的,除非作为函数调用运算符的左操作数,例如 (p->*pmf)(args)。
[编辑] Void 表达式
返回 void 的函数调用表达式,转换为 void 的转换表达式,以及 throw-expression 都被归类为 prvalue 表达式,但它们不能用于初始化引用或作为函数参数。它们可以用于被丢弃值的上下文(例如,单独一行,作为逗号运算符的左操作数等)以及返回 void 的函数中的 return 语句。此外,throw-expression 可以用作条件运算符 ?: 的第二个和第三个操作数。
void 表达式没有结果对象。 |
(C++17 起) |
[编辑] 位域
指定位域的表达式(例如 a.m,其中 a 是类型 struct A { int m: 3; } 的左值)是一个 glvalue 表达式:它可以作为赋值运算符的左操作数,但不能获取其地址,并且不能将非 const 左值引用绑定到它。const 左值引用或右值引用可以从位域 glvalue 初始化,但会创建一个位域的临时副本:它不会直接绑定到位域。
可移动表达式虽然由任何变量名称组成的表达式是左值表达式,但如果它出现在以下操作数中,则该表达式可能是可移动的 如果一个表达式是可移动的,那么为了重载决议的目的,它既被视为右值又被视为左值(C++23 前)被视为右值(C++23 起)(因此它可能选择移动构造函数)。详见局部变量和参数的自动移动。 |
(C++11 起) |
[编辑] 历史
[编辑] CPL
编程语言 CPL 首先引入了表达式的值类别:所有 CPL 表达式都可以以“右值模式”求值,但只有某些类型的表达式在“左值模式”下有意义。当以右值模式求值时,表达式被视为计算值的规则(右侧值,或 rvalue)。当以左值模式求值时,表达式实际上给出一个地址(左侧值,或 lvalue)。这里的“左”和“右”代表“赋值的左侧”和“赋值的右侧”。
[编辑] C
C 编程语言采用了类似的分类法,只是赋值的作用不再重要:C 表达式分为“左值表达式”和其他(函数和非对象值),其中“左值”表示标识对象的表达式,“定位器值”[4]。
[编辑] C++98
2011 年之前的 C++ 遵循 C 模型,但将“rvalue”这个名称恢复给非左值表达式,将函数变为左值,并添加了规则:引用可以绑定到左值,但只有 const 引用可以绑定到右值。一些非左值 C 表达式在 C++ 中成为左值表达式。
[编辑] C++11
随着 C++11 中移动语义的引入,值类别被重新定义以描述表达式的两个独立属性[5]
在 C++11 中,那些
- 有身份且不能移动的表达式称为左值(lvalue)表达式;
- 有身份且可以移动的表达式称为将亡值(xvalue)表达式;
- 没有身份但可以移动的表达式称为纯右值(prvalue)表达式;
- 没有身份且不能移动的表达式不被使用[6]。
具有身份的表达式称为“glvalue 表达式”(glvalue 代表“广义左值”)。左值和将亡值都是 glvalue 表达式。
可以从中移动的表达式称为“rvalue 表达式”。纯右值和将亡值都是 rvalue 表达式。
[编辑] C++17
在 C++17 中,在某些情况下拷贝消除被强制执行,这要求将 prvalue 表达式与其初始化的临时对象分离,从而形成了我们今天的系统。请注意,与 C++11 的方案相反,prvalue 不再从中移动。
[编辑] 脚注
- ↑ 假设 i 具有内置类型或前置增量运算符重载为返回左值引用。
- ↑ 2.0 2.1 2.2 2.3 特殊右值类别,参见待决成员函数调用。
- ↑ 假设 i 具有内置类型或者后置增量运算符未重载为返回左值引用。
- ↑ “C 社区内部对于左值(lvalue)的含义存在分歧,一方认为左值是任何形式的对象定位符,另一方则认为左值在赋值运算符的左侧有意义。C89 委员会采纳了将左值定义为对象定位符的定义。”—— ANSI C 原理,6.3.2.1/10。
- ↑ Bjarne Stroustrup 的 “新”值术语,2010 年。
- ↑ const prvalues(仅允许用于类类型)和 const xvalues 不绑定到
T&&
重载,但它们绑定到 const T&& 重载,这些重载也被标准归类为“移动构造函数”和“移动赋值运算符”,满足此分类目的的“可移动”定义。然而,此类重载不能修改其参数,在实践中不使用;在没有它们的情况下,const prvalues 和 const xvalues 绑定到 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++ 标准。
缺陷报告 | 应用于 | 发布时的行为 | 正确的行为 |
---|---|---|---|
CWG 616 | C++11 | 通过成员访问和成员访问 右值指向成员的结果是纯右值 |
重新分类为将亡值 |
CWG 1059 | C++11 | 数组纯右值不能被 cv 限定 | 允许 |
CWG 1213 | C++11 | 对数组右值进行下标操作导致左值 | 重新分类为将亡值 |
[编辑] 另请参见
C 文档 关于 值类别
|
[编辑] 外部链接
1. | C++ 值类别和 decltype 揭秘 — David Mazières, 2021 | |
2. | 经验性地确定表达式的值类别 — StackOverflow |