左值与右值
左值:表示的是可以获取地址的表达式(有名字)。能出现在赋值语句的左边。
右值:表示无法获取地址的对象。如常量值、函数返回值、lambda表达式等。无法获取地址,但不代表不能改变,可以通果定义右值的右值引用来更改右值。
注:如下代码虽然可能能通过编译,但是切记右值不能出现在左边
string s1("Hello");
string s2("World");
s1 + s2 = s2;
cout<<"s1:"<<s1<<endl; //s1:Hello
cout<<"s2:"<<s2<<endl; //s2:World
string() = "World"; //竟然可以对临时对象(右值)赋值
左值引用和右值引用
左值引用:传统的C++中引用被称为左值引用
右值引用:C++11中增加了右值引用,右值引用关联到右值时,右值被存储到特定位置,右值引用指向该特定位置,也就是说,右值虽然无法获取地址,但是右值引用是可以获取地址的,该地址表示临时对象的存储位置。
注:When the right-hand side of an assignment is an rvalue, then the left-hand side object can steal resources from the right-hand side object rather than perfroming a separate allocation, thus enabling move semantics.
当赋值语句的右边是一个右值(rvalue),左边的对象可以偷右边对象的资源,而不需要分配内存(拷贝内容也不需要),从而实现完美转发和移动语义。如下例,函数返回值是右值,j是右值引用。
int&& j = getInt();
与如下拷贝赋值(需要给变量j分配内存)区别:
int j = getInt();
一个左值可以通过std::move()实现移动语义
int a = 10; //a是左值
int b(std::move(a)); //现在b是右值引用,这样使用的前提是必须要保证a不会再被使用
不完美转发与完美转发
不完美转发
forward(2); //forward(int&&):2里面调用process(int&):2,应该调用process(int&&)
//在转发的过程中右值变成了左值
完美转发
通过std::forward()实现完美转发
右值引用的特点
- 通过右值引用的声明,右值生命周期与右值引用类型变量的生命周期一样长,只要右值引用变量还存在,右值临时量将会一直存在;
- 右值引用独立与左值和右值,右值引用类型的变量可能是左值也可能是右值;
- T&& t在发生类型推断的时候,是左值还是右值取决于它的初始化
代码示例
注:常量左值引用是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。不过常量左值所引用的右值在它的“余生”中只能是只读的。
相对地,非常量左值只能接受非常量左值对其进行初始化。无论是常量右值引用还是非常量右值引用都不能用左值引用初始化或赋值。
#include <bits/stdc++.h>
using namespace std;
template<typename T>
void fun(T&& t)
{
cout << t << endl;
}
int getInt()
{
return 5;
}
int main() {
int a = 10;
int& b = a; //b是左值引用
int& c = 10; //错误,c是左值不能使用右值初始化
int&& d = 10; //正确,右值引用用右值初始化
int&& e = a; //错误,e是右值引用不能使用左值初始化
const int& f = a; //正确,左值常引用相当于是万能型,可以用左值或者右值初始化
const int& g = 10;//正确,左值常引用相当于是万能型,可以用左值或者右值初始化
const int&& h = 10; //正确,右值常引用
const int& aa = h;//正确
int& i = getInt(); //错误,i是左值引用不能使用临时变量(右值)初始化
int&& j = getInt(); //正确,函数返回值是右值
fun(10); //此时fun函数的参数t是右值
fun(a); //此时fun函数的参数t是左值
return 0;
}
总结
C++11通过引入右值引用来优化性能,具体来说是通过移动语义来避免无谓拷贝(unnecessary copying)的问题,通过move语义来将临时生成的左值中的资源无代价的转移到另外一个对象中去,通过完美转发来解决不能按照参数实际类型来转发的问题(同时,完美转发获得的一个好处是可以实现移动语义)。