C++中的万能引用和完美转发
- 阅读这篇博文需要了解C++中的左值(lvalue)和右值(rvalue)的概念,详情参见我的另外一篇博文:C++移动语义及拷贝优化
- 万能引用和完美转发多涉及到模板的使用,如若不是自己写模板,则可不用关心
万能引用(Universal Reference)
首先,我们来看一个例子:
1 |
|
这样例子的编译输出都没有什么问题,但是如果我们修改成下面的调用方式呢?
1 | int main() { |
则会得到一个大大的编译错误。因为上面的模板函数只能接受左值或者左值引用(左值一般是有名字的变量,可以取到地址的),我们当然可以重载一个接受右值的模板函数,如下也可以达到效果。
1 | template<typename T> |
输出结果:
1 | 传入的是左值 |
第一次函数调用的是左值得版本,第二次函数调用的是右值版本。但是,有没有办法只写一个模板函数即可以接收左值又可以接收右值呢?
C++ 11中有万能引用(Universal Reference)的概念:使用T&&
类型的形参既能绑定右值,又能绑定左值。
但是注意了:只有发生类型推导的时候,T&&才表示万能引用;否则,表示右值引用。
所以,上面的案例我们可以修改为:
1 | template<typename T> |
引用折叠(Universal Collapse)
万能引用说完了,接着来聊引用折叠(Univers Collapse),因为完美转发(Perfect Forwarding)的概念涉及引用折叠。一个模板函数,根据定义的形参和传入的实参的类型,我们可以有下面四中组合:
- 左值-左值 T& & # 函数定义的形参类型是左值引用,传入的实参是左值引用
- 左值-右值 T& && # 函数定义的形参类型是左值引用,传入的实参是右值引用
- 右值-左值 T&& & # 函数定义的形参类型是右值引用,传入的实参是左值引用
- 右值-右值 T&& && # 函数定义的形参类型是右值引用,传入的实参是右值引用
但是C++中不允许对引用再进行引用,对于上述情况的处理有如下的规则:
所有的折叠引用最终都代表一个引用,要么是左值引用,要么是右值引用。规则是:如果任一引用为左值引用,则结果为左值引用。否则(即两个都是右值引用),结果为右值引用。
即就是前面三种情况代表的都是左值引用,而第四种代表的右值引用。
完美转发(Perfect Forwarding)
下面接着说完美转发(Perfect Forwarding),首先,看一个例子:
1 |
|
猜一下,上面的输出结果是什么?
1 | 传入的是左值 |
是不是和我们预期的不一样,下面我们来分析一下原因:
warp()
函数本身的形参是一个万能引用,即可以接受左值又可以接受右值;第一个warp()
函数调用实参是左值,所以,warp()
函数中调用func()
中传入的参数也应该是左值;第二个warp()
函数调用实参是右值,根据上面所说的引用折叠规则,warp()函数接收的参数类型是右值引用,那么为什么却调用了调用
func()的左值版本了呢?这是因为在warp()
函数内部,左值引用类型变为了右值,因为参数有了名称,我们也通过变量名取得变量地址。
那么问题来了,怎么保持函数调用过程中,变量类型的不变呢?这就是我们所谓的“完美转发”技术,在C++11中通过std::forward()
函数来实现。我们修改我们的warp()
函数如下:
1 | template<typename T> |
则可以输出预期的结果。