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