Value categories
每个 C++ 表达式 (包含操作数的运算符、字面量、变量名等)都具有两个独立属性: 类型 和 值类别 。每个表达式都具有某种非引用类型,且每个表达式必定属于三大基本值类别之一: prvalue 、 xvalue 和 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; // 错误,对函数进行赋值 } |
- 一个 右值 是纯右值或亡值;
| 扩展内容 |
|---|
|
之所以在历史上被称为右值,是因为它们可以出现在赋值表达式的右侧。但通常情况并非总是如此: |
注意:此分类体系在过去的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:: hex 。即使变量的类型是右值引用,由其名称构成的表达式仍是左值表达式(但请参阅 可移动表达式 );
| 扩展内容 |
|---|
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 ] 和 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; // 模板参数是左值 } int a{3}; // 静态变量,编译期已知固定地址 void foo() { set<a>(); }
|
(since C++11) |
属性:
- 与 泛左值 (见下文)相同。
- 左值的地址可通过内置取址运算符获取: & ++ i [1] 和 & std:: hex 均为合法表达式。
- 可修改的左值可作为内置赋值运算符及复合赋值运算符的左操作数。
- 左值可用于 初始化左值引用 ;这将使新名称与表达式所标识的对象建立关联。
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 为纯右值;
- 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说明符中使用时除外)。 - 纯右值不能具有 抽象类类型 或其数组类型。
xvalue
下列表达式属于 xvalue表达式 :
-
a.
m
,即
对象成员
表达式,其中
a
为右值且
m为对象类型的非静态数据成员; -
a.
*
mp
,即
对象成员指针
表达式,其中
a
为右值且
mp为指向数据成员的指针; - a, b ,即内置 逗号 表达式,其中 b 为亡值;
- a ? b : c ,即特定 b 和 c 条件下的 三元条件 表达式(详见 定义 );
|
(C++11 起) |
|
(C++17 起) |
|
(C++23 起) |
属性:
- 与右值(见下文)相同。
- 与泛左值(见下文)相同。
具体而言,与所有右值一样,xvalue 可绑定到右值引用;与所有泛左值一样,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 {}; // 示例来自 C++23 标准:7.2.1 值类别 [basic.lval] struct A { int m; }; A&& operator+(A, A); A&& f(); int main() { A a; A&& ar = static_cast<A&&>(a); // 返回类型为右值引用的函数调用是 xvalue static_assert(is_xvalue<decltype( (f()) )>::value); // 对象表达式的成员,对象为 xvalue,`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` 是 lvalue,`&ar` 是有效的 static_assert(is_lvalue<decltype( (ar) )>::value); [[maybe_unused]] A* ap = &ar; } |
混合类别
glvalue
一个 glvalue 表达式 要么是左值,要么是亡值。
属性:
- glvalue 可通过左值到右值、数组到指针或函数到指针的 隐式转换 隐式转换为 prvalue。
- glvalue 可以是 多态 的:其标识对象的 动态类型 不一定与表达式的静态类型相同。
- 在表达式允许的情况下,glvalue 可以具有 不完整类型 。
右值
一个 右值表达式 要么是纯右值,要么是亡值。
属性:
- 右值的地址无法通过内置取址运算符获取: & int ( ) 、 & i ++ [3] 、 & 42 以及 & std :: move ( x ) 均无效。
- 右值不能作为内置赋值或复合赋值运算符的左操作数。
- 右值可用于 初始化常量左值引用 ,此时该右值所标识的临时对象的生命周期将 被延长 至引用作用域结束。
| (since C++11) |
特殊类别
待决成员函数调用
表达式
a.
mf
和
p
-
>
mf
(其中
mf
是
非静态成员函数
),以及表达式
a.
*
pmf
和
p
-
>
*
pmf
(其中
pmf
是
成员函数指针
),均被归类为纯右值表达式。但这些表达式不能用于初始化引用、作为函数参数,或用于除函数调用运算符左操作数之外的任何用途,例如:
(
p
-
>
*
pmf
)
(
args
)
。
Void 表达式
返回 void 的函数调用表达式、转换为 void 的强制转换表达式以及 throw 表达式 均被归类为纯右值表达式,但它们不能用于初始化引用或作为函数参数。这些表达式可用于弃值上下文(例如单独成行、作为逗号运算符的左操作数等),也可用于返回 void 的函数中的 return 语句。此外,throw 表达式可作为 条件运算符 ?: 的第二和第三操作数使用。
|
Void表达式没有 结果对象 。 |
(since C++17) |
位域
指定位域(例如 a. m ,其中 a 是类型为 struct A { int m : 3 ; } 的左值)的表达式是泛左值表达式:它可以作为赋值运算符的左操作数,但不能获取其地址,且非 const 左值引用无法绑定到它。const 左值引用或右值引用可以从位域泛左值初始化,但会创建位域的临时副本:引用不会直接绑定到位域本身。
可移动表达式尽管由任何变量名组成的表达式都是左值表达式,但当其作为以下操作数出现时,此类表达式可能具备可移动性: 若表达式具备可移动性,在 重载决议 时它会被视作 右值或左值 (C++23 前) 右值 (C++23 起) (因此可能选择 移动构造函数 )。详见 从局部变量和参数的自动移动 。 |
(C++11 起) |
历史
CPL
编程语言 CPL 首次引入了表达式的值类别:所有CPL表达式都可以在"右值模式"下求值,但只有特定类型的表达式在"左值模式"下才有意义。在右值模式下求值时,表达式被视为计算值的规则(即右值,或 rvalue )。在左值模式下求值时,表达式实际给出一个地址(即左值,或 lvalue )。这里的"左"和"右"分别代表"赋值语句左侧"和"赋值语句右侧"。
C
C语言遵循了类似的分类体系,但赋值操作的作用已不再显著:C语言表达式被划分为"左值表达式"和其他类型(函数与非对象值),其中"左值"指代能够标识对象的表达式,即"定位符值" [4] 。
C++98
2011年前的C++沿用了C语言的模型,但恢复了"右值"这一名称用于指代非左值表达式,将函数设为左值,并新增了规则:引用可以绑定到左值,但只有常量引用才能绑定到右值。若干在C语言中属于非左值的表达式在C++中成为了左值表达式。
C++11
随着 C++11 中移动语义的引入,值类别被重新定义以表征表达式的两个独立属性 [5] :
- 具有标识 :可以确定表达式是否与另一个表达式引用同一实体,例如通过比较对象的地址或它们所标识的函数(直接或间接获取);
- 可被移动 : 移动构造函数 、 移动赋值运算符 ,或其他实现移动语义的函数重载可以绑定到该表达式。
在 C++11 中,表达式满足以下条件:
- 具有标识且不可被移动的表达式称为 左值 表达式;
- 具有标识且可被移动的表达式称为 亡值 表达式;
- 不具有标识且可被移动的表达式称为 纯右值 表达式;
- 不具有标识且不可被移动的表达式未被使用 [6] 。
具有标识符的表达式被称为“泛左值表达式”(glvalue 是“generalized lvalue”的缩写)。左值和亡值都属于泛左值表达式。
能够被移动的表达式称为"右值表达式"。prvalue 和 xvalue 都属于右值表达式。
C++17
在 C++17 中, 拷贝消除 在某些场景下变为强制要求,这需要将纯右值表达式与其初始化的临时对象分离开来,从而形成了我们当前的体系。需要注意的是,与 C++11 的方案不同,纯右值不再被从中移出。
脚注
- ↑ 假设 i 具有内置类型,或前置递增运算符被 重载 为返回左值引用。
- ↑ 2.0 2.1 2.2 2.3 特殊右值类别,参见 待决成员函数调用 。
- ↑ 假设 i 具有内置类型,或后置递增运算符未被 重载 为返回左值引用。
- ↑ "C语言社区内部关于左值含义存在分歧,一派认为左值可以是任何类型的对象定位器,另一派则认为左值在赋值运算符左侧才具有意义。C89委员会采纳了将左值定义为对象定位器的方案。" -- ANSI C原理说明,6.3.2.1/10。
- ↑ 《新值分类术语》 ,Bjarne Stroustrup,2010年。
-
↑
常量纯右值(仅允许类类型)和常量亡值不会绑定到
T&&重载,但会绑定到 const T && 重载(这些重载也被标准归类为"移动构造函数"和"移动赋值运算符"),从而满足本分类中"可被移动"的定义。然而此类重载无法修改其参数且在实践中不被使用;当缺少这些重载时,常量纯右值和常量亡值会绑定到 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
|