左值 右值 一
https://en.cppreference.com/w/cpp/language/value_category
值的类别
任何C++的表达式(带有变量名、操作数等的操作符)都可以特征化为两个相互独立的属性:类型和值类别。每一个表达式都有非引用类型,每一个表达式都严格属于三个几本值类别中的一个:prvalue(纯右值) xvalue(亡值)和lvalue(左值)
-
glvalue(“generalized” lvalue)(泛左值)是一个表达式,它的求值决定了一个对象,位运算或者函数;
-
prvalue(“pure” rvalue)(纯右值)是一个表达式,表达式有下面两种:
- 计算一个操作符的运算对象,或者一个void表达式(这个右值没有结果对象)
- 初始化一个对象或者位字段(这个纯右值被称作有一个结果对象)。除了
decltype
,所有的类和数组都有纯右值结果对象,就算被丢弃也是有的。这个结果对象可以是一个变量,一个用new表达式创建的对象,一个通过临时实例化创建的临时对象,或者是他们中的一个成员。
-
xvalue(an “eXpiring” value)(到期的值,亡值)是一个泛左值,表明一个结构体或者位字段的资源可以被重新利用
-
lvalue(历史遗留称谓,表示可以出现在赋值表达式左边)(左值)是一个glvalue(泛左值),不是xvalue(亡值)
-
rvalue(历史遗留称谓,表示可以出现在赋值表达式右边)(右值)是一个prvalue(纯右值)或者xvalue(亡值)
注意: 这个分类与C++以前的标准版本区别比较大,详情参考下面信息
基本分类
lvalue 左值
如下表达式是左值表达式:
-
变量名、函数名、模板形参的名字或者数据成员的名字,不论其类型,比如
std::cin
std::endl
。即使变量的类型是右值的引用,由其名字组成的表达式仍然是左值表达式 -
返回类型是左值引用的函数调用或者操作符重载,比如
std::getline(std::cin, str)
std::cout << 1
str1 = str2
++it
-
a = b
a += b
a %= b
,所有内置的赋值和混合赋值表达式 -
++a
--a
,内置的前置自增和前置自减表达式 -
*p
,内置的间接取值表达式 -
a[n]
p[n]
,内置的下标索引表达式,当a[n]
的一个操作对象是一个数组的lvalue(左值)时(since C++11) -
a.m
,一个对象成员的表达式,除了m是一个枚举类型的成员或者是一个非静态成员函数,或者a是一个右值,但是m是一个对象的非静态数据成员 -
p->m
,内置的指针成员表达式,除了m是一个枚举成员或者非静态函数 -
a.*mp
,对象的成员指针,a是一个lvalue(左值),mp是一个数据成员指针 -
p->*mp
,内置的指向成员指针的指针表达式,mp是一个数据成员指针 -
a, b
,内置的逗号表达式,b是一个lvalue(左值) -
a ? b : c
,对于某些b和c的三元条件表达式(比如,当它们都是lvalues(左值)) -
一个字符串,比如
Hello, world!
-
转换成lvalue(左值)引用类型的转换表达式,比如
static_cast<int&>(x)
-
返回类型是一个函数左值引用的函数调用或者操作符重载表达式(since C++11)
-
转换为函数rvalue(右值)引用的表达式,比如
static_cast<void (&&)(int)>(x)
(since C++11)
特性
- 与glvalue(泛左值)相同
- 可以用内置的取址表达式取址:
&++i
&std::endl
都是合法的 - 可修改的lvalue(左值)可以用作内置赋值或者混合赋值表达式的左边
- lvalue(左值)可以初始化lvalue(左值)引用;创建一个新的名字用来关联表达式定义的对象
prvalue 纯右值
下面的表达式是纯右值
-
字面意思的数据(除了字符串之外,比如
Hello
),比如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是一个枚举成员或者非静态成员函数,或者a是一个rvalue(右值)并且m是一个非引用类型的非静态数据成员(until C++11) -
p->m
,内置的指针成员表达思,m是一个枚举成员或者非静态成员函数 -
a.*mp
,对象成员指针表达式,mp是一个成员函数,或者a是一个rvalue(右值)并且mp是一个数据成员指针(until C++11) -
p->*mp
内置的指针成员的指针表达式,mp是指向成员函数的指针 -
a, b
,内置逗号表达式,b是rvalue(右值) -
a ? b : c
,对于某些b和c的三元判断表达式 -
非引用类型的转换,比如
static_cast<double>(x)
std::string{} -
this
指针 -
枚举项
-
非类型模板参数,除非类型是类或者(since C++20)lvalue(左值)引用类型
-
lambda表达式,比如
[](int x){ return x * x; }
(since C++11) -
requires
表达式,比如requires (T i) { typename T::type; }
(since C++20) -
特殊化的概念,比如
std::equality_comparable<int>
(since C++20)
特性
-
与rvalue(右值)一样
-
prvalue(纯右值)没有多态:它所标识的对象的动态类型,取决于表达式的类型
-
非类非数组的prvalue(纯右值)不能被cv(
const
volatile
)限定。(注意:一个函数调用或者转换表达式可以产生非类的prvalue(纯右值)的cv限定类型,但是cv限定很快就会被去除) -
prvalue(纯右值)不能有非完整类型(除了
void
,或者在decltype
中使用) -
prvalue(纯右值)不能有抽象类类型或者数组类型
xvalue 亡值
下面的表达式是xvalue(亡值)表达式:
-
返回类型是rvalue(右值)引用的函数调用或者重载的操作符,比如
std::move(x)
-
a[n]
,内置的下标索引表达式,其操作对象是数组的rvalue(右值) -
a.m
,对象成员表达式,a是rvalue(右值),m是一个非索引类型的非静态的数据成员 -
a.*mp
,对象的一个成员指针表达式,a是rvalue(右值)并且mp是数据成员指针 -
a ? b : c
,对于某些b和c的三元条件表达式 -
对于对象类型rvalue(右值)索引转换,比如
static_cast<char&&>(x)
-
任何确定临时对象的表达式,在临时实例化完成后(since C++17)
特性
- 与rvalue(右值)一样
- 与glvalue(泛左值)一样
与所有的rvalues(右值)一样,xvalues(亡值)可以绑定到rvalue(右值)引用;与所有的glvalues(泛左值)一样,xvalues(亡值)可以有多态性,非类的xvalues(亡值)可以有cv限定。
混合分类
glvalue 泛左值
一个glvalue(泛左值)表达式可以是lvalue(左值)或者xvalue(亡值)
特性
-
glvalue(泛左值)可以隐式的转化为prvalue(纯右值),比如lvalue(左值)到rvalue(右值),数组到指针,函数到指针的隐式转换
-
glvalue(泛左值)可以是多态的:对象的东太太类型不必是表达式的静态类型
-
glvalue(泛左值)可以有未完成的类型,只要表达式允许
rvalue 右值
一个rvalue(右值)表达式可以是prvalue(纯右值)也可以是xvalue(亡值)
特性
-
rvalue(右值)的地址不能通过内置的取地址符进行操作:
&int()
&i++
&42
&std::move(x)
都是非法的 -
rvalue(右值)不能放到内置的赋值操作符或者混合赋值操作符的左侧
-
rvalue(右值)可以用在初始化const类型的lvalue(左值)索引,这种情况下,这个rvalue(右值)对象的生命周期被延长为索引的作用域结尾
-
rvalue(右值)可以用作初始化rvalue(右值)索引,这种情况下,这个rvalue(右值)对象的生命周期被延长到索引的作用域结尾(since C++11)
-
当用做函数实参,并且有两个重载的函数可用,一个是传递rvalue(右值)引用,另一个是传递const lvalue(左值)引用,rvalue(右值)将会绑定到rvalue(左值)引用的重载上(因此,当拷贝和移动构造函数都可用时,rvalue(右值)的参数调用移动构造函数,拷贝和移动赋值操作与此类似)(since C++11)
特殊分类
为决定成员的函数调用
a.mf
p->mf
,当mf是非静态成员函数,并且表达式a.*pmf
p->*pmf
,pmf是成员函数指针,被认为是prvalue(纯右值)表达式,但是它们不能用作初始化引用,不能用作函数的参数。除了用作函数调用操作符左侧参数(比如(p->*pmf)(args)
),不能用作其他任何目的
void
返回void的函数调用表达式,转换为void的表达式,抛出异常的表达式都被定义为prvalue(纯右值),但是它们不能用作初始化引用,或者函数参数。可以用在舍弃值的语境下(比如,在一行,作为逗号操作符的左侧操作数)和函数返回void的返回语句中。另外,抛出异常的语句可以应做三元运算符?:的第二个和第三个操作数
void表达式没有结果对象(since C++17)
位字段
代表位字段的表达式(比如,a.m
,a是一个结构体struct A { int m: 3; }
的lvalue(左值))是一个glvalue(泛左值)表达式:他可以用作赋值操作符的左侧,但是它的地址不能获得,并且不能绑定非const lvalue(左值)引用。const lvalue(左值)引用或者rvalue(右值)引用可以通过位字段glvalue(泛左值)初始化,但是会有一个对于位字段的临时拷贝,不能直接绑定位字段
历史
CPL
CPL是第一个为表达式引入值类别的:所有的CPL表达式都能以“右侧模式”求值,但是只有一些表达式在“左侧模式”有意义。当在右侧模式求值时,表达式被当作求值的规则(右侧的值,或者右值)。当在左侧求值时,表达式被给定一个明确的地址(左侧的值或者左值)。左和右在这里表示赋值左侧和赋值右侧
C
C语言沿用了相似的分类,除了赋值角色不再那么重要:C语言的表达式被归类为左值表达式和其他(函数和非对象值),左值表示定义一个对象的表达式,一个定位器的值
C++98
在2011之前的C++沿用了C的模式。但是恢复了非左值表达式名字为右值,令函数为左值,增加了可以绑定引用到左值上,但是只有const引用可以绑定到右值上。一些非左值C语言表达式在C++中成为了左值表达式
C++11
随着移动语义添加到C++11,值的分类被重新定义,用来区分表达式的两种独立属性
-
拥有身份:用来确定表达式是否与另一表达式有同一实体,比如通过比较对象或者函数的地址(直接或间接的)
-
可被移动:移动构造函数,移动赋值操作或者另一个重载的函数实现了移动语义,可以绑定到这个表达式
在 C++11中:
- 拥有身份且不可被移动的表达式被称作左值 (lvalue)表达式;
- 拥有身份且可被移动的表达式被称作亡值 (xvalue)表达式;
- 不拥有身份且可被移动的表达式被称作纯右值 (prvalue)表达式;
- 不拥有身份且不可被移动的表达式无法使用
拥有身份的表达式被称作“泛左值 (glvalue) 表达式”。左值和亡值都是泛左值表达式。
可被移动的表达式被称作“右值 (rvalue) 表达式”。纯右值和亡值都是右值表达式。
C++17
C++17 中,某些场合强制要求进行复制消除,而这要求将纯右值表达式从被它们所初始化的临时对象中分离出来,这就是我们现有的系统。要注意,与 C++11 的方案相比,纯右值已不再是可被移动。