C++的左值和右值是比较有理解难度的概念。每个 C++ 表达式都有一个类型,属于某个值类别。 值类别是编译器在表达式计算期间创建、复制和移动临时对象时必须遵循的规则的基础。
在C++11中,值类型有以下分类:
prvalue 是一个表达式,计算结果可以用于初始化对象或位域,或计算运算符的操作数值
xvalue 是一个表达式,表示一个对象或位域,该对象或位域的资源可重复使用(通常是因为它接近其生存期的末尾)。 比如:
std::move
的结果,和返回右值引用的函数lvalue 是一个表达式,计算结果可以确定对象、位域或函数的标识
例子
为了方便理解, 下面可以看看一些例子:
#include <iostream>
#include <utility>
class result {
public:
int a;
() : a(0) {
resultstd::cout << "result" << std::endl;
}
~result() {
std::cout << "~result" << std::endl;
}
};
&& func(){
result ;
result rreturn std::move(r);
}
int main() {
std::cout << "first line of main" << std::endl;
;
result a; // lvalue
a(); // prvalue
result&& r = result(); // prvalue
result&& r = func(); // xvalue
resultstd::cout << "last line of main" << std::endl;
}
我们可以看到, a
因为表达式的结果是可以获取地址的,所以a
属于lvalue
,result()
因为计算结果是没有地址的,所以属于prvalue
,而func()
返回的是快要消亡的具名对象,所以func()
返回的是xvalue
。
右值生命周期延长规则
C++11以上标准规定,当右值引用(T&&
)或const左值引用(const T&
)绑定prvalue时,临时对象的生命周期会被延长至引用的作用域结束。
const std::string& s1 = "Hello"; // 临时字符串生命周期延长至s1作用域结束,内容不可变
std::string&& s2 = std::string("World"); // 临时对象生命周期延长至s2作用域结束,内容可变
C++17 中 prvalue的变化
到了C++17, prvalue被弱化成只初始化对象,不再是不能被移动的临时对象。
比如下面的代码
struct NonCopyable {
operator int() {
return 0;
}
() = default;
NonCopyable(const NonCopyable &) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
NonCopyable};
int main() {
[[maybe_unused]] int i = NonCopyable{}; //1
[[maybe_unused]] NonCopyable v = NonCopyable{}; //2
}
在C++17以前,2是不行的,这是因为 C++11 要求 RVO 或者 NRVO 也一定是需要有复制 / 移动构造函数的。而到了C++17,2是可以的,是因为 prvalue 仅仅只是一个初始化器,不是对象,不会发生拷贝,C++编译器会直接让prvalue对象在v的原地初始化,等价于NonCopyable v{}
。 在汇编层面,会在进入函数之前传递v对象的地址,进入构造函数以后原地初始化。