C++
January 20, 2019

C++中的万能引用和完美转发

C++中的万能引用和完美转发

  1. 阅读这篇博文需要了解C++中的左值(lvalue)和右值(rvalue)的概念,详情参见我的另外一篇博文:C++移动语义及拷贝优化
  2. 万能引用和完美转发多涉及到模板的使用,如若不是自己写模板,则可不用关心

万能引用(Universal Reference)

首先,我们来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

using std::cout;
using std::endl;


template<typename T>
void func(T& param) {
cout << param << endl;
}


int main() {
int num = 2019;
func(num);
return 0;
}

这样例子的编译输出都没有什么问题,但是如果我们修改成下面的调用方式呢?

1
2
3
4
int main() {
func(2019);
return 0;
}

则会得到一个大大的编译错误。因为上面的模板函数只能接受左值或者左值引用(左值一般是有名字的变量,可以取到地址的),我们当然可以重载一个接受右值的模板函数,如下也可以达到效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<typename T>
void func(T& param) {
cout << "传入的是左值" << endl;
}
template<typename T>
void func(T&& param) {
cout << "传入的是右值" << endl;
}



int main() {
int num = 2019;
func(num);
func(2019);
return 0;
}

输出结果:

1
2
传入的是左值
传入的是右值

第一次函数调用的是左值得版本,第二次函数调用的是右值版本。但是,有没有办法只写一个模板函数即可以接收左值又可以接收右值呢?

C++ 11中有万能引用(Universal Reference)的概念:使用T&&类型的形参既能绑定右值,又能绑定左值。

但是注意了:只有发生类型推导的时候,T&&才表示万能引用;否则,表示右值引用。

所以,上面的案例我们可以修改为:

1
2
3
4
5
6
7
8
9
10
11
12
template<typename T>
void func(T&& param) {
cout << param << endl;
}


int main() {
int num = 2019;
func(num);
func(2019);
return 0;
}

引用折叠(Universal Collapse)

万能引用说完了,接着来聊引用折叠(Univers Collapse),因为完美转发(Perfect Forwarding)的概念涉及引用折叠。一个模板函数,根据定义的形参和传入的实参的类型,我们可以有下面四中组合:

但是C++中不允许对引用再进行引用,对于上述情况的处理有如下的规则:

所有的折叠引用最终都代表一个引用,要么是左值引用,要么是右值引用。规则是:如果任一引用为左值引用,则结果为左值引用。否则(即两个都是右值引用),结果为右值引用

即就是前面三种情况代表的都是左值引用,而第四种代表的右值引用。

完美转发(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
26
27
#include <iostream>

using std::cout;
using std::endl;

template<typename T>
void func(T& param) {
cout << "传入的是左值" << endl;
}
template<typename T>
void func(T&& param) {
cout << "传入的是右值" << endl;
}


template<typename T>
void warp(T&& param) {
func(param);
}


int main() {
int num = 2019;
warp(num);
warp(2019);
return 0;
}

猜一下,上面的输出结果是什么?

1
2
传入的是左值
传入的是左值

是不是和我们预期的不一样,下面我们来分析一下原因:

warp()函数本身的形参是一个万能引用,即可以接受左值又可以接受右值;第一个warp()函数调用实参是左值,所以,warp()函数中调用func()中传入的参数也应该是左值;第二个warp()函数调用实参是右值,根据上面所说的引用折叠规则,warp()函数接收的参数类型是右值引用,那么为什么却调用了调用func()的左值版本了呢?这是因为在warp()函数内部,左值引用类型变为了右值,因为参数有了名称,我们也通过变量名取得变量地址。

那么问题来了,怎么保持函数调用过程中,变量类型的不变呢?这就是我们所谓的“完美转发”技术,在C++11中通过std::forward()函数来实现。我们修改我们的warp()函数如下:

1
2
3
4
template<typename T>
void warp(T&& param) {
func(std::forward<T>(param));
}

则可以输出预期的结果。