在C语言中,我们常常会提起左值(lvalue)、右值(rvalue)这样的称呼。而在编译程序时,编译器有时也会在错误的信息中包含左值、右值的说法。不过左值、右值通常不是通过一个严谨的定义而为人所知的,大多数时候左右值的定义与判别方法是一体的。一个典型的判别方法就是,在赋值表达式中,出现在等号左边的就是“左值”,而在等号右边的,则成为“右值”。比如:
a = b + c
在这个赋值表达式中,a就是一个左值,而b+c则是一个右值。这种识别左值、右值的方法在C++中依然有效。不过C++中还有一个被广泛认同的说法,那就是可以取地址的、有名字的就是左值,反之不能取地址的、没有名字的就是右值。那么这个加法赋值表达式中,&a是允许的操作,但&(b+c)这样的操作则不会通过编译。因此a是一个左值,(b+c)是一个右值。
这些判别方法通常都非常有效。更为细致地,在C++11中,右值是由两个概念构成的,一个将亡值(xvalue,eXpiring Value),另一个则是纯右值(prvalue,Pure RValue)。
其实纯右值就是C++98标准中右值的概念,讲的是用于辨识临时变量和一些不跟对象关联的值,比如
- 非引用返回的函数返回的临时变量值
- 一些算数表达式,比如1 + 3产生的临时变量值
- 不与对象关联的字面量值,比如:2、‘c’、true
- 类型转换函数的返回值
- Lambda表达式
而将亡值则是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移位他用),比如
- 返回右值引用T&&的函数返回值
- std::move的返回值
- 转换为T&&的类型转换函数的返回值
而剩余的,可以标识函数、对象的值都属于左值。在C++11的程序中,所有的值必属于左值、将亡值、纯右值三者之一。
通常情况下,右值引用是不能绑定到任何的左值的。而在C++98标准中就已经出现的左值引用是否可以绑定到右值(由右值进行初始化)?比如:
T &e = ReturnRvalue();
const T &F = ReturnRvalue();
这样的语句是否能够通过编译呢?这里的答案是:e的初始化会导致编译时错误(注:我在VS2017上是可以编译通过),而f则不会。
出现这样的状况的原因是,在常量左值引用在C++98标准中开始就是个“万能”的引用类型。他可以接受非常量左值、常量左值、右值对其进行初始化。而且在使用右值对其初始化的时候,常量左值引用还可以像右值引用一样将右值的生命期延长。不过相比于右值引用所引用的右值,常量左值所引用的右值在它的“余生”中只能是只读的。相对地,非常量左值只能接受非常量左值对其进行初始化。
在C++98通过左值引用来绑定一个右值的情况并不少见,比如:
const bool & judgement = true;
const bool judgement = true;
可能很多程序员都没有注意到其中的差别(从语法上讲,前者直接使用了右值并为其“续命”,而后者的右值在表达式结束后就销毁了)。
事实上,即使在C++98中,我们也常可以使用常量引用来减少临时对象的开销,例子如下:
class Copyable
{
public:
Copyable() {};
Copyable(const Copyable&)
{
cout << "Copied" << endl;
}
};
Copyable ReturnRvale()
{
return Copyable();
}
void AcceptVal(Copyable )
{
}
void AcceptRef(const Copyable &)
{
}
int main()
{
cout << "Pass by Value" << endl;
AcceptVal(ReturnRvale());
cout << "Pass by Ref" << endl;
AcceptRef(ReturnRvale());
return 0;
}
AcceptVal使用了值传递参数,而AcceptRef使用了引用传递。在以ReturnRvalue返回的右值为参数的时候,AcceptRef就可以直接使用产生的临时值(并延长其生命期),而AcceptVal则不能直接使用临时对象。
结果如下:
Pass by Value
Copied
copied
Pass by Ref
Copied
在C++11中,同样地如果以右值引用为参数声明如下函数:
void AcceptRvalueRef(Copyable &&){}
这样也可以减少临时变量拷贝的开销,还可以在AcceptRvalueRef中修改改临时值。
参考:《深入理解C++11》