左值右值
传统的左值和右值划分
- 左值:英文为 left value, 简写lvalue
- 右值:英文为 right value, 简写rvalue
一个左一个右, 这个左右的判定是针对什么呢?
实际上是针对等号来实现判定的
int x;
x = 3;
x = 3这个表达式,x可以放在左边,也可以放在右边,这个表达式的含义就是把3赋予x
如果我们此时写3 = x,这样是无意义的,因为我们并不能把x赋予3
因此我们把能放在等号左边的称之为左值,相对应的,不能放在等号左边的称为右值
以上是c语言定义的左值和右值,但是c++呢,对c语言的左值和右值进行了拓展
实际上,左值不一定能放在左边,右值也可能放在等号左边
我们一直说左值右值,但是这个概念其实并不是针对某个对象/数值
什么意思呢?
在上面的代码片段中,int x, x是一个对象,也是一个变量,你说x是左值还是右值呢?这种说法本身就是错误的
因为只有这个对象作为表达式存在且同时对这个对象所表达的表达式进行求值之后,得到的结果是左值还是右值
因此:左值和右值是针对表达式或者表达式的求值结果
划分可见下图:
是一个树形结构,其中就有l value和rvalue
glvalue
- glvalue: generalized lvalue, 泛左值,是一个左值的扩展的集合,泛左值是一个表达式,这个表达式的求值结果能确定一个对象,位域或函数
具体来看:
x = 3;
这个代码片段是一个语句,语句里有一个赋值表达式,赋值表达式里还包含了两个子表达式:x和3
通常来说,我们需要对赋值语句进行求值,那么需要先对x*和3**进行求值
对x求值的结果是啥?
这句话的含义并不是说,我要访问x的内存,获取其中的值,而是说广义的求值,而是说,我们需要获得x关联的那块内存, 这两者是有区别的
那么回过来理解泛左值,也就是说,我们通过这个内存,来确定这个x,这就是泛左值,简单来说,就是一种标识的作用
再比如:
int y;
y = 3;
和上面的代码片段几乎一样,只是赋值的对象不一样,一个是处理x对应的内存,一个是处理y所对应的内存
也就是说,x和y都是glvalue
prvalue
纯右值:和泛左值相对,也是一个表达式,但是这个表达式符合以下的两种情况之一
- 作为某个运算符的操作数或者void表达式(例如返回void的函数)
- 初始化某个对象/位域
先看第一点:
x = 3;
赋值表达式有两个操作数,一个是x,一个是3
其中x是gvalue, 3是prvalue
为什么呢?
因为这个3只能作为运算符的操作数来使用,你总不能这样写吧:3 = x
3只能做一般意义上的操作数,类似3 + 2
, 3 / 2
这种
再看第二点:
int x = 3;
这个语句是合法的,但是这个语句里不再是赋值操作符,而是初始化操作, 表示我们要执行拷贝初始化
此时这个3,只能用于初始化这个对象
3和x不同,3只能有上述的两种用法,但是x却可以标识一块内存地址
再看一个例子
struct Str
{
};
int main()
{
int{};
Str{};
}
上面的代码片段,都是构造出一个临时对象,这种临时对象,也被用来操作符的操作数或者用于初始化,因此也归属于纯右值
xvalue
将亡值:expiring, 其含义为:代表其资源能够被重新使用的对象或者位域的泛左值
举个例子:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> x;
return 0;
}
这个x能保存int类型的数据,将这些数据放在一串连续的内存中
那么这和将亡值又有什么关系呢?
比如说:
void func(vector<int>&& par)
{
}
int main()
{
vector<int> x;
func(std::move(x));
return 0;
}
std::move的含义是转换成将亡值
这表明了,后续的代码,不会对x中包含的资源做任何行为,因为x即将死亡
总结
- glvalue:标识对象,其中包含了lvalue和xvalue
- lvalue: 也是标识一个对象/位域/函数,其次,不是xvalue, 也就是说,一个gvalue并不会即将死亡, 资源还是属于对象,别人无法偷走,那么就是lvalue
- rvalue:要么是一个将亡值,确实标识一个对象/函数/位域,但是很快就会消亡(xvalue)或者作为操作符的操作数/初始化操作的操作数(prvalue)
那么回过头来:在c++中,左值不一定能放在等号左边,右值也可能放在等号左边
前半句:
const int x = 3;
const int y = 2;
定义了一个常量x,这个常量作为表达式处理,表达式求值以后,得到的是左值还是右值呢?很显然是一个左值
因为x的确标识了一个对象, x和y是两个不同的对象, 所以x属于glvalue
那么x是将亡值吗?
显然不是,因为我们后续还是可以使用x,因此,x属于glvalue的lvalue
那么我们肯定不能x = 4
做这种操作,因为x是常量,不能放在等号的左边,不能被修改
后半句:
struct Str
{
};
int main()
{
int x = int{};
Str y = Str{};
}
上面的代码片都是使用纯右值来初始化一个左值
那么,有意思的来了:
Str () = Str();
// Str {} = Str();
// Str {} = Str{}
// Str () = Str{};
我们上面说到,这个临时对象属于纯右值,但是如你所见,这可以放在等号的左边
首先,这个临时对象没有标识某个对象/位域/函数,因此不属于glvalue, 因此属于rvalue
左值和右值的转换
左值转右值
int x = 3;
int y = x;
y = x;
x + y;
从表达式的角度来理解,用3初始化x,并用x初始化y
注意到没有?上面说到,纯右值的作用有一条就是初始化
而且纯右值刚好还有一个作用是作为操作符的操作数
但是x和y显然是一个左值,但是这个代码又是合法的,并且和我上面的有冲突,我讲错了吗?
显然不是的,是因为,左值可以转换成右值
在一个表达式中的某个地方需要一个右值,那么我们可以提供一个左值,编译器会自动转换成一个右值
再看一个临时具体化的例子(Temporary Materializetion):从Prvalue到xvalue的转换
struct Str
{
int x;
};
int main()
{
Str(); // 纯右值
Str().x; // xvalue
}
有意思,太有意思了
.
操作符前面是一个临时对象,纯右值,后面是一个成员x
, 它的含义是从临时对象中获取特定的部分(x)
也就是说,此时Str()
可以理解为一种广义的对象,类似于之前代码片段的x
或者y
Str()
标识了某块内存,这个时候,我们就不能简单的理解为:
- 初始化
- 操作符的操作数
这种简单的划分了
而是说,把它视为一个将亡值,因为将亡值属于glvalue,这样才能确定一个对象/位域/函数
还有其他例子吗?
void func(const int& par)
{
}
int main()
{
func(3);
return 0;
}
这个代码片段是合法的,我们用3来初始化par,从引用的角度来理解,引用是要绑定到某个具体的对象上面的,但是3是一个纯右值
我们并不认为3标识了一个对象,但是这个代码片段又是合法的
其实就是因为Temporary Materializetion, 把3转换成了一个将亡值
decltype
当我们理解了左值和右值的概念以后,再回头看decltype
当时是从类型推动的角度来讨论的,见【重学c++Primer】第二章
现在我们从左值右值的角度来看看, decltype对表达式的处理
- prvalu -> T
- lvalue -> T&
- xvalue -> T&&
这里使用c++ insights来查看decltype的推导过程