左值右值
左值右值,经常听的东西,于是稍微记录 —— (实际上看了 Efficient C++ 之后,结合一些新标准,觉得还是有必要写写)
所有权,资源转移,移动语义等蹲一下新坑,现在先聚焦一下左右值以及左右值引用。
左值与右值
左值
先说左值,表示了一个占据内存中某个可识别的位置(也就是一个地址)的对象的表达式
比如说 int x = 10;,其中的 x 就是一个非常标准且简单的左值 —— 有名字有内存位置有地址
右值
和左值相对的,它没有地址,比如临时对象等
std::string name = std::string("Alice"); 此处的std::string("Alice"); 就是一个经典的右值,一个临时对象
再比如x + 1, std::move(name)
当然,从C++11开始引入了纯右值和将亡值的概念,
纯右值 prvalue
10
std::string("Alice")
x + 1这些通常是纯右值。
它们更像是“临时计算出来的值”。
将亡值 xvalue
语义上表示一个生命将尽的资源,常用move进行构造
std::move(name)它不是创建了一个新的临时对象,而是把已有对象 name 转换成一个“可以被移动”的表达式。
直觉
左值一般来说是长期存在,可以用于资源拷贝; 右值一般来说是临时对象,即将被销毁,不过可以被“移动”
引用
又扯到了引用,常见的 ——
T& value;
const T& value;
T&& value;左值引用
T& value,实际上是“左值的引用”,或者我们常说的取别名,此处它只能被左值赋值,比如 ——
int a = 10;
int& x = a; // 合法
int& x = 10; // 非法所以同样的,在函数参数中 ——
void Foo(int& a);
Foo(x); // 合法
Foo(10); // 非法此时的语义为"我借用了这个变量,并且会修改它"
不过有一个宽松的“例外”——(Unlimited Rule!少数例外原则
就是使用 const 修饰的左值引用,因为加上 const 之后语义变成了—— “只读这个东西,但是不修改”。
const int&x = 10; // 合法
Foo(const int& a);
Foo(10); // 合法实际上它可以类似理解于
const T temp = rvalue;
const T& value = temp;假设对于局部引用const std::string& ref = std::string("Alice");
那么临时对象Alice的生命周期就会被延长和ref一致
右值引用
T&& value
const T& value 可以传入右值,但是问题也显而易见,就是传入的只读资源是不可消费的,于是提出了右值引用。
void Take(std::string&& s) { std::cout << s << "\n"; }
Take(std::string("Alice")); // 合法
Take(name); // 非法
Take(std::move(name)); // 合法move
std::move() 表示把一个左值转化成右值表达式,比如std::move(name),就是把原先的name左值转化成一个右值表达式。
不过值得注意的,std::move只会做表达式的转化,而不会真的移动资源
比如
std::string name = "Alice";
std::string other = std::move(name);此处std::move只是把name做了转化,而真正把右值表达式进行消费的是other的移动构造函数
值得注意的 —— 此时name依旧是一个可以使用的对象,但是不要假定它的值还是Alice,也就是之后的代码不要依赖name == Alice这个逻辑来进行。不过可以重新赋值等继续使用。
右值引用变量本身是左值
说起来非常的拗口,实际上就是 —— 哪怕声明的是右值引用变量,但是在使用的时候会被当做左值使用。
还是很绕,例子 ——
void Take(std::string&& s) { std::string other = s; }此处other的实例化会触发拷贝构造,因为 s 是左值表达式,尽管它在传入的时候被声明成了右值引用变量
所以如果想要使用移动构造,则需要写
void Take(std::string&& s) { std::string other = std::move(s); }