(一)什么是左值和右值?
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们 之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
1.左值
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左 值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
特别注意:
const修饰后的左值,不能给他赋值,但它依然是左值。
int main()
{// 以下的p、b、c、*p都是左值int* p = new int(0);int b = 1;const int c = 2;// 以下几个是对上面左值的左值引用int*& rp = p;int& rb = b;const int& rc = c;int& pvalue = *p;return 0;
}
2.右值
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引 用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
注意:
引用右值是“&&“, 右值的引用是左值(例如下面中的rr1,rr2,rr3都是左值)。
double fmin(double x, double y)
{return x > y ? y : x;
}int main()
{double x = 1.1, y = 2.2;// 以下几个都是常见的右值10;x + y;fmin(x, y);// 以下几个都是对右值的右值引用int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y); //fmin函数会返回一个临时变量,这个变量是右值// 这里编译会报错:error C2106: “=”: 左操作数必须为左值10 = 1;x + y = 1;fmin(x, y) = 1;return 0;
}
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可 以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地 址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇, 这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。
int main()
{double x = 1.1, y = 2.2;int&& rr1 = 10;const double&& rr2 = x + y;rr1 = 20;rr2 = 5.5; // 报错return 0;
}
(二)左值引用与右值引用比较
1.左值引用:
1. 左值引用只能引用左值,不能引用右值。
2. 但是const左值引用既可引用左值,也可引用右值。
int main() {// 左值引用只能引用左值,不能引用右值。int a = 10;int& ra1 = a; // ra为a的别名//int& ra2 = 10; // 编译失败,因为10是右值// const左值引用既可引用左值,也可引用右值。const int& ra3 = 10;const int& ra4 = a;return 0; }
2.右值引用:
1. 右值引用只能右值,不能引用左值。
2. 但是右值引用可以move以后的左值。
int main() {// 右值引用只能右值,不能引用左值。int&& r1 = 10;// error C2440: “初始化”: 无法从“int”转换为“int &&”// message : 无法将左值绑定到右值引用int a = 10;int&& r2 = a;// 右值引用可以引用move以后的左值int&& r3 = std::move(a);//move(a)之后,a并不是转换成了右值,a仍然为左值//(简单的)可以理解为调用move函数,move返回了一个和a数据一摸一样的右值。return 0;}
注意:
move(a)并不是将a变成了左值,(可以这么理解)而是该函数返回了一个与a数据一样匿名对象,但是对该右值进行操作会影响到a。
(三) 右值引用使用场景和意义
左值引用的短板:
当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回, 只能传值返回。
例如下面的:bit::string to_string(int value)函数中可以看到,这里只能使用传值返回, 传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。
新的编译器会对上面的构造进行优化(出现连续的构造函数时,编译器会进行优化)
移动语义
移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。
这里to_string函数生成了一个str,这个str是左值,正常来说,to_string返回时,会再次调用构造函数生成一个临时对象,但是这里编译器做了优化(str被识别成将亡值,to_string返回时编译器会将str识别成右值,进行返回),所以ret2的初始化操作省去了生成匿名对象,只有一个str的深拷贝构造和一个移动语义。
简单来说,就是将str当成了右值进行ret2的初始化操作,所以只有一个移动语义
不仅仅有移动构造,还有移动赋值:
// 移动赋值 string& operator=(string&& s) {cout << "string& operator=(string&& s) -- 移动语义" << endl;swap(s);return *this; } int main() {bit::string ret1;ret1 = bit::to_string(1234);return 0; } // 运行结果: // string(string&& s) -- 移动语义 // string& operator=(string&& s) -- 移动语义
这里运行后,我们看到调用了一次移动构造和一次移动赋值。
因为如果是用一个已经存在的对象接收,编译器就没办法优化了。bit::to_string函数中会先用str生成构造生成一个临时对象,但是 我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时 对象做为bit::to_string函数调用的返回值赋值给ret1,这里调用的移动赋值。
简单来说,这里将str识别成右值,然后调用移动构造生成临时对象进行赋值操作,所以这里有一个一次移动构造和一次移动赋值
Move()函数
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能 真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move 函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性, 它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。
如下面例子:
int main() {bit::string s1("hello world");// 这里s1是左值,调用的是拷贝构造bit::string s2(s1);// 这里我们把s1 move处理以后, 会被当成右值,调用移动构造// 但是这里要注意,一般是不要这样用的,因为我们会发现s1的// 资源被转移给了s3,s1被置空了。bit::string s3(std::move(s1));return 0; }
需要注意的是:
move函数将s1转化为右值(返回一个s1的右值),但是s1本身还是左值。
还有下面这个例子:
(四)完美转发
1.万能引用(引用折叠)
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力。
void Fun(int& x) { cout << "左值引用" << endl; } void Fun(const int& x) { cout << "const 左值引用" << endl; } void Fun(int&& x) { cout << "右值引用" << endl; } void Fun(const int&& x) { cout << "const 右值引用" << endl; } // 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。 // 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力, // 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值, // 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发 template<typename T> void PerfectForward(T&& t) {Fun(t); } int main() {PerfectForward(10); // 右值int a;PerfectForward(a); // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b); // const 左值PerfectForward(std::move(b)); // const 右值return 0; }
运行结果:
可以看到不管是左值还是右值,引用后的对象都为左值(即 t 是左值);
如果我们保留原来的属性呢?
因此有了std::forward 完美转发在传参的过程中保留对象原生类型属性
2.完美转发实际中的使用场景:
简单举个例子(该例子存在问题,只是帮助理解使用场景):
void Insert(Node* pos, T&& x); void Insert(Node* pos, const T& x);template<class T> void PushFront(T&& x) {//Insert(_head->_next, x);Insert(_head->_next, std::forward<T>(x)); }int main() {PushFront("2222");return 0; }
这里我们调用PushFront(),即可以传左值,也可以传右值,因为有完美转发,和forward()。