上文讲了什么是右值引用。右值引用(rvalue reference)是 C++11 为了实现移动语意(move semantic)和完美转发(perfect forwarding)而提出来的。
右值引用,简单说就是绑定在右值上的引用。右值的内容可以直接移动(move)给左值对象,而不需要进行开销较大的深拷贝(deep copy)。
在面向对象中,有的类是可以拷贝的,例如车、房等他们的属性是可以复制的,可以调用拷贝构造函数,有点类的对象则是独一无二的,或者类的资源是独一无二的,比如 IO 、 std::unique_ptr等,他们不可以复制,但是可以把资源交出所有权给新的对象,称为可以移动的。
C++11最重要的一个改进之一就是引入了移动语义,这样在一些对象的构造时可以获取到已有的资源(如内存)而不需要通过拷贝,申请新的内存,这样移动而非拷贝将会大幅度提升性能。例如有些右值即将消亡析构,这个时候我们用移动构造函数可以接管他们的资源。
C++11用std::move来实现移动语义。下边看一个小栗子,请忽略std::forward:
#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));
}
int main() {
std::cout << "传递右值:" << std::endl;
pass(1);
std::cout << "传递左值:" << std::endl;
int v = 1;
pass(v);
return 0;
}
输出:
传递右值:
普通传参:左值引用
std::move 传参:右值引用
std::forward 传参:右值引用
传递左值:
普通传参:左值引用
std::move 传参:右值引用
std::forward 传参:左值引用
可能有的小伙伴看出来异样,传递右值时:
-
pass函数明明传入一个右值,但是打印出左值引用。
-
右值通过std::move就变成了右值引用。
传递左值时:
1. 但是打印了左值引用。
2. 左值引用通过std::move就变成了右值引用。
这两个问题2都好解释,std::move函数就是为了移动而生。
下边说一下传递右值的异常。
我们来看这个模板,编译器将T推导为 int& 类型。当我们用 int& 替换掉 T 后,得到 int & &&。看起来两个&&被消除了。
但是我们尝试写下边代码的时候却报错了!!!!
int & &rra = ra; // 编译器报错:不允许使用引用的引用!
编译器真是双标党,编译器不允许我们自己把代码写成int& &&,它自己却这么干了 =。=
那么 int & &&到底是个什么东西呢?!它就是引用折叠,也有人叫它引用坍缩,C++对于这种类型的折叠有下边的规则:
函数形参类型 | 实参参数类型 | 推导后函数形参类型 |
T& | 左引用 | T& |
T& | 右引用 | T& |
T&& | 左引用 | T& |
T&& | 右引用 | T&& |
编译器不允许我们写下类似int & &&这样的代码,但是它自己却可以推导出int & &&代码出来。它的理由就是:我(编译器)虽然推导出T为int&,但是我在最终生成的代码中,利用引用折叠规则,将int & &&等价生成了int &。推导出来的int & &&只是过渡阶段,最终版本并不存在。所以也不算破坏规定咯。
所有的引用折叠最终都代表一个引用,要么是左值引用,要么是右值引用。规则就是:如果任一引用为左值引用,则结果为左值引用。否则(即两个都是右值引用),结果为右值引用。 《Effective Modern C++》
为了解决引用折叠引入的问题,一个新的概念--完美转发就闪亮登场了。
还是刚才的小程序,std::forward 传参: 输出的都是我们想要的类型。
std::forward
的源码形式大致是这样:
/*
* 精简了标准库的代码,在细节上可能不完全正确,但是足以让我们了解转发函数 forward 的了
*/
template<typename T>
T&& forward(T ¶m)
{
return static_cast<T&&>(param);
}
我们来仔细分析一下这段代码, 可以看到,不管T是值类型,还是左值引用,还是右值引用,T&经过引用折叠,都将是左值引用类型。也就是forward 以左值引用的形式接收参数 param, 然后 通过将param进行强制类型转换 static_cast<T&&> (),最终再以一个 T&&返回,根据上边的引用折叠规则,就能完美的forward传参。