右值引用&&
简介
右值引用 (Rvalue Referene) 是 C++ 11 中引入的新特性 ,可以获取一个将亡值,右值引用的声明让这个临时值的声明周期得以延长,只要变量还活着,那么将亡值也将继续存活。
它实现了移动语义 (Move Sementics) 和完美转发 (Perfect Forwarding)。它的主要目的有两个方面:
- 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
- 能够更简洁明确地定义泛型函数。
使用方法
右值引用标记为T&&。
c++提供了std::move
这个方法将左值参数无条件的转化为右值。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include <iostream> #include <string> void reference(std::string& str) { std::cout << "左值" << std::endl; } void reference(std::string&& str) { std::cout << "右值" << std::endl; } int main() { std::string lv1 = "string,"; std::string&& rv1 = std::move(lv1); std::cout << rv1 << std::endl;
const std::string& lv2 = lv1 + lv1; std::cout << lv2 << std::endl;
std::string&& rv2 = lv1 + lv2; rv2 += "Test"; std::cout << rv2 << std::endl;
reference(rv2);
return 0; }
|
rv2 虽然引用了一个右值,但由于它是一个引用,所以 rv2 依然是一个左值。
注意,这里有一个很有趣的历史遗留问题,我们先看下面的代码:
1 2 3 4 5 6 7 8
| #include <iostream> int main() { const int &b = std::move(1);
std::cout << a << b << std::endl; }
|
为什么不允许非常量引用绑定到非左值?这是因为这种做法存在逻辑错误:
1 2 3 4 5 6 7 8 9
| void increase(int & v) { v++; } void foo() { double s = 1; increase(s); }
|
由于 int& 不能引用 double 类型的参数,因此必须产生一个临时值来保存 s 的值, 从而当 increase() 修改这个临时值时,从而调用完成后 s 本身并没有被修改。
移动语义move
传统的C++ 没有区分移动
以及拷贝
的概念,造成了大量的数据浪费,浪费时间和空间。move 是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。 例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #include <iostream> class A { public: int *pointer; A():pointer(new int(1)) { std::cout << "构造" << pointer << std::endl; } A(A& a):pointer(new int(*a.pointer)) { std::cout << "拷贝" << pointer << std::endl; } A(A&& a):pointer(a.pointer) { a.pointer = nullptr; std::cout << "移动" << pointer << std::endl; } ~A() { std::cout << "析构" << pointer << std::endl; delete pointer; } };
A return_rvalue(bool test) { A a,b; if(test) return a; else return b; } int main() { A obj = return_rvalue(false); std::cout << "obj:" << std::endl; std::cout << obj.pointer << std::endl; std::cout << *obj.pointer << std::endl; return 0; }
|
在上面的代码中:
- 首先会在 return_rvalue 内部构造两个 A 对象,于是获得两个构造函数的输出;
- 函数返回后,产生一个将亡值,被 A 的移动构造(A(A&&))引用,从而延长生命周期,并将这个右值中的指针拿到,保存到了 obj 中,而将亡值的指针被设置为 nullptr,防止了这块内存区域被销毁。
再看下面这个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include <iostream> #include <utility> #include <vector> #include <string> int main() { std::string str = "Hello world."; std::vector<std::string> v;
v.push_back(str); std::cout << "str: " << str << std::endl;
v.push_back(std::move(str)); std::cout << "str: " << str << std::endl;
return 0; }
|
完美转发forward
前面提到了,一个声明的右值引用其实是左值。这就对参数转发产生了问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| void reference(int& v) { std::cout << "左值" << std::endl; } void reference(int&& v) { std::cout << "右值" << std::endl; } template <typename T> void pass(T&& v) { std::cout << "普通传参:"; reference(v); } int main() { std::cout << "传递右值:" << std::endl; pass(1);
std::cout << "传递左值:" << std::endl; int l = 1; pass(l);
return 0; }
|
对于 pass(1) 来说,虽然传递的是右值,但由于 v 是一个引用,所以同时也是左值。 因此 reference(v) 会调用 reference(int&),输出『左值』。 而对于pass(l)而言,l是一个左值,为什么会成功传递给 pass(T&&) 呢?
这是基于引用坍缩规则的:在传统 C++ 中,我们不能够对一个引用类型继续进行引用, 但 C++ 由于右值引用的出现而放宽了这一做法,从而产生了引用坍缩规则,允许我们对引用进行引用, 既能左引用,又能右引用。但是却遵循如下规则:
函数形参类型 |
实参参数类型 |
推导后函数形参类型 |
T& |
左引用 |
T& |
T& |
右引用 |
T& |
T&& |
左引用 |
T& |
T&& |
右引用 |
T&& |
更准确的讲,无论模板参数是什么类型的引用,当且仅当实参类型为右引用时,模板参数才能被推导为右引用类型。 这才使得 v 作为左值的成功传递。
完美转发就是基于上述规律产生的。所谓完美转发,就是为了让我们在传递参数的时候, 保持原来的参数类型(左引用保持左引用,右引用保持右引用)。 为了解决这个问题,应当使用std::forward
进行参数转发:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include <iostream> #include <utility> void reference(int& v) { std::cout << "左值引用" << std::endl; } void reference(int&& v) { std::cout << "右值引用" << std::endl; } template <typename T> void pass(T&& v) { std::cout << " 普通传参: "; reference(v); std::cout << " std::move 传参: "; reference(std::move(v)); std::cout << " std::forward 传参: "; reference(std::forward<T>(v)); std::cout << "static_cast<T&&> 传参: "; reference(static_cast<T&&>(v)); } int main() { std::cout << "传递右值:" << std::endl; pass(1);
std::cout << "传递左值:" << std::endl; int v = 1; pass(v);
return 0; }
|
输出为:
1 2 3 4 5 6 7 8 9 10
| 传递右值: 普通传参: 左值引用 std::move 传参: 右值引用 std::forward 传参: 右值引用 static_cast<T&&> 传参: 右值引用 传递左值: 普通传参: 左值引用 std::move 传参: 右值引用 std::forward 传参: 左值引用 static_cast<T&&> 传参: 左值引用
|
std::forward 和 std::move 一样,没有做任何事情,std::move 单纯的将左值转化为右值, std::forward 也只是单纯的将参数做了一个类型的转换,从现象上来看, std::forward(v) 和 static_cast<T&&>(v) 是完全一样的。