值类别
每个 C++ 表达式(带有操作数的操作符、字面量、变量名等)可按照两种独立的特性加以辨别:类型 和值类别 (value category)。每个表达式都具有某种非引用类型,且每个表达式只属于三种基本值类别中的一种:纯右值 (prvalue)、亡值 (xvalue)、左值 (lvalue)。
- 泛左值 (glvalue)(“泛化 (generalized)”的左值)是一个求值可确定某个对象或函数的标识的表达式;
- 纯右值 (prvalue)(“纯 (pure)”的右值)是求值符合下列之一的表达式:
- 计算某个运算符的操作数的值(这种纯右值没有结果对象)
- 初始化某个对象(称这种纯右值有一个结果对象)。
延伸内容 |
---|
如此称呼的历史原因是,左值可以在赋值表达式的左边出现。一般来说,并不总是这样: void foo(); void baz() { int a; // 表达式 `a` 是左值 a = 4; // OK,可以出现在赋值表达式的左侧 int& b{a}; // 表达式 `b` 是左值 b = 5; // OK,可以出现在赋值表达式的左侧 const int& c{a}; // 表达式 `c` 是左值 c = 6; // 非良构,对只读引用赋值 // 表达式 `foo` 是左值 // 可以用内建取地址运算符获取其地址 void (*p)() = &foo; foo = baz; // 非良构,对函数赋值 } |
- 右值 (rvalue) 是纯右值或者亡值;
延伸内容 |
---|
如此称呼的历史原因是,右值可以在赋值表达式的右边出现。一般来说,并不总是这样: |
注意:这个分类法与 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); } |
基本类别
左值
下列表达式是左值表达式:
- 变量、函数、模板形参对象 (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` 是左值,模板形参对象 } |
- 返回类型是左值引用的函数调用或重载运算符表达式,例如 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()` 是左值,此函数调用的返回类型是左值引用 } |
- a = b,a += b,a %= b,以及所有其他内建的赋值及复合赋值表达式;
- ++a 和 --a,内建的前置自增与前置自减表达式;
- *p,内建的间接寻址表达式;
- a[n] 和 n[a],内建的下标表达式,当
a[n]
中的操作数是数组左值时 (C++11 起); - 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:成员函数指针 } struct foo { static void m() {} // 静态成员函数 }; void baz() { foo a; void (*p1)() = &a.m; // `a.m` 是左值 void (*p2)() = &foo::m; // 相同 } |
- 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; // 模板形参为左值 } int a{3}; // 静态变量,编译时已知其固有地址 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() { // 不是左值,`v` 是标量类型 int 的模板形参 const int* a = &v; // 非良构 v = 3; // 非良构:需要左值作为赋值的左操作数 }
|
(C++11 起) |
|
(C++20 起) |
性质:
- 与右值相同(见下文)。
- 纯右值不具有多态:它所标识的对象的动态类型始终是该表达式的类型。
- 非类非数组的纯右值不能有 cv 限定,除非它被实质化以绑定到 cv 限定类型的引用 (C++17 起)。(注意:函数调用或转型表达式可能生成非类的 cv 限定类型的纯右值,但它的 cv 限定符通常被立即剥除。)
- 纯右值不能具有不完整类型(除了类型 void(见下文),或在
decltype
说明符中使用之外) - 纯右值不能具有抽象类类型或它的数组类型。
亡值
下列表达式是亡值表达式:
- 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 {}; // 来自 C++23 标准的示例: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; } |
混合类别
泛左值
泛左值表达式包括左值、亡值。
性质:
右值
右值表达式包括纯右值、亡值。
性质:
- 右值不能由内建的取址运算符取地址:&int()、&i++[3]、&42 及 &std::move(x) 是非法的。
- 右值不能用作内建赋值运算符及内建复合赋值运算符的左操作数。
- 右值可以用来初始化 const 左值引用,这种情况下该右值所标识的对象的生存期被延长到该引用的作用域结尾。
(C++11 起) |
特殊类别
未决成员函数调用
表达式 a.mf 与 p->mf,其中 mf
是非静态成员函数,以及表达式 a.*pmf 与 p->*pmf,其中 pmf
是成员函数指针,被归类为纯右值表达式,但它们不能用来初始化引用,作为函数实参,或者用于除了作为函数调用运算符的左操作数(例如 (p->*pmf)(args))以外的任何目的。
void 表达式
返回 void 的函数调用表达式,转换到 void 的转型表达式,以及 throw 表达式,被归类为纯右值表达式,但它们不能用来初始化引用或者作为函数实参。它们可以用在舍弃值的语境(例如自成一行,作为逗号运算符的左操作数等)和返回 void 的函数中的 return 语句中。另外,throw 表达式可用作条件运算符 ?:
的第二个和第三个操作数。
void 表达式没有结果对象。 |
(C++17 起) |
位域
代表某个位域的表达式(例如 a.m,其中 a 是类型 struct A { int m: 3; } 的左值)是泛左值表达式:它可用作赋值运算符的左操作数,但它不能被取地址,并且非 const 的左值引用不能绑定于它。const 左值引用或右值引用可以从位域泛左值初始化,但这会制造位域的临时副本:它不会直接绑定到位域。
有移动资格的表达式虽然由任何变量的名字构成的表达式是左值表达式,但若它作为 的操作数出现,则表达式具有移动资格。 如果表达式有移动资格,那么它在重载决议时被视为左值或者右值 (C++23 前)右值 (C++23 起)(因此可能会选择移动构造函数)。详情见自动从局部变量和形参移动。 |
(C++11 起) |
历史
CPL
编程语言 CPL 率先为表达式引入了值类别:所有 CPL 表达式都能以“右侧模式 (right-hand mode)”求值,但只有某些类型的表达式在“左侧模式 (left-hand mode)”有意义。在右侧模式中求值时,表达式被当做一条进行值的计算(右侧值,或右值)的规则。在左侧模式中求值时,表达式的效果是给出地址(左侧值,或左值)。“左”和“右”代表“赋值之左”和“赋值之右”。
C
C 编程语言遵循相似的分类法,但赋值的作用不再重要:C 的表达式被分为“左值 (lvalue) 表达式”和其他(函数和非对象值),其中“左值 (lvalue)”的含义为标识对象的表达式,即“定位器值 (locator value)”[4]。
C++98
2011 年前的 C++ 遵循 C 模型,但恢复了对非左值表达式的“右值 (rvalue)”称呼,令函数为左值,并添加了引用能绑定到左值但唯有 const 的引用能绑定到右值的规则。几种非左值的 C 表达式在 C++ 中成为了左值表达式。
C++11
随着移动语义引入到 C++11 之中,值类别被重新进行了定义,以区别表达式的两种独立的性质[5]:
- 拥有身份 (identity):可以确定表达式是否与另一表达式指代同一实体,例如通过比较它们所标识的对象或函数的(直接或间接获得的)地址;
- 可被移动:移动构造函数、移动赋值运算符或实现了移动语义的其他函数重载能够绑定于这个表达式。
C++11 中:
- 拥有身份且不可被移动的表达式被称作左值 (lvalue) 表达式;
- 拥有身份且可被移动的表达式被称作亡值 (xvalue) 表达式;
- 不拥有身份且可被移动的表达式被称作纯右值 (prvalue) 表达式;
- 不拥有身份且不可被移动的表达式无法使用[6]。
拥有身份的表达式被称作“泛左值 (glvalue) 表达式”。左值和亡值都是泛左值表达式。
可被移动的表达式被称作“右值 (rvalue) 表达式”。纯右值和亡值都是右值表达式。
C++17
C++17 中,某些场合强制要求进行复制消除,而这要求将纯右值表达式从被它们所初始化的临时对象中分离出来,这就是我们现有的系统。要注意,与 C++11 的方案相比,纯右值已不再是可被移动。
脚注
- ↑ 假设 i 具有内建类型,或者它的前置自增运算符被重载为返回左值引用。
- ↑ 2.0 2.1 2.2 2.3 一种特殊的右值类别,参见未决成员函数调用。
- ↑ 假定 i 具有内建类型,或它的后置自增运算符并未重载为返回左值引用。
- ↑ “C 社区中对此的观点主要围绕着 lvalue 的含义而有所差异,一组人认为 lvalue 是任何种类的对象定位器,另一组人认为 lvalue 在赋值运算符的左侧时才有意义。C89 委员会采纳了作为对象定位器的定义。”—— ANSI C 基本原理,6.3.2.1/10 。
- ↑ 新·值术语,Bjarne Stroustrup,2010。
- ↑ const 纯右值(仅允许类类型)及 const 亡值不能绑定于
T&&
的重载,但它们可以绑定于 const T&& 的重载——由于它满足这种分类中的“可被移动”的定义,标准中将这种重载也归类为“移动构造函数”和“移动赋值运算符”。然而,这种重载无法修改它们的参数,并且在实践中并不使用;当没有这种重载时,const 纯右值和 const 亡值绑定于 const T& 的重载。
引用
- C++23 标准(ISO/IEC 14882:2024):
- 7.2.1 Value category [basic.lval]
- C++20 标准(ISO/IEC 14882:2020):
- 7.2.1 Value category [basic.lval]
- C++17 标准(ISO/IEC 14882:2017):
- 6.10 Lvalues and rvalues [basic.lval]
- C++14 标准(ISO/IEC 14882:2014):
- 3.10 Lvalues and rvalues [basic.lval]
- C++11 标准(ISO/IEC 14882:2011):
- 3.10 Lvalues and rvalues [basic.lval]
- C++98 标准(ISO/IEC 14882:1998):
- 3.10 Lvalues and rvalues [basic.lval]
缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
缺陷报告 | 应用于 | 出版时的行为 | 正确行为 |
---|---|---|---|
CWG 616 | C++11 | 右值的成员访问及通过成员指针的成员访问的结果是纯右值 | 重分类为亡值 |
CWG 1059 | C++11 | 数组纯右值不能有 cv 限定 | 可以有 cv 限定 |
CWG 1213 | C++11 | 数组右值的下标操作导致左值 | 重分类为亡值 |
参阅
外部链接
1. | David Mazières, 2021 - C++ 值类别和 decltype 解读 | |
2. | StackOverflow, 2013 - 凭经验确定表达式的值类别
|