所谓完美转发, 是指在函数模板中,完全依照模板的参数类型,将参数传递给函数模板中调用的另一个函数。
引入
看个例子:
template<typename T>
void IamForwording(T t) {
IrunCodeActually(t);
}
上例中,IamForwording是一个转发函数模板。而函数IrunCodeActually是真正执行代码的目标函IamForwording数。对于IrunCodeActually而言,它总是希望转发函数IamForwording将参数按照传入IamForwording时的类型传递,而不产生额外的开销,就好像转发者不存在一样。
但是上例中,IamForwording的参数会导致参数在传给IrunCodeActually之前产生额外的临时对象拷贝。这样的转发只能说是正确的转发,谈不上完美。
所以通常要做到完美转发,需要传递的是一个引用类型,引用类型不会有额外的开销。
其次,要考虑转发函数对类型的接收能力。因为目标函数可能需要即接收左值引用,又接受右值引用。那么如果转发函数只能接受其中的一部分,我们也无法做到完美转发。当然,你可能会想到使用常量左值类型(即能接受左值引用,又能接受右值引用)作为转发函数的类型,但这可能会遇到一些问题:
void IrunCodeActually(int t) {}
template<typename T>
void IamForwording(const T& t) {
IrunCodeActually(t);
}
这里,由于目标函数的参数类型是非常量的左值引用类型,因此无法接收常量左值引用作为参数(这个问题的可以用重载来解决,但是这可能会造成代码冗余)。同理,当目标函数的参数是个右值引用,同一无法接受任何左值类型作为参数,间接的,也就导致无法使用移动语句。
那C++11是如何解决完美转发的问题呢?实际上,C++是通过引入一条所谓引用折叠的新语言规则,并结合新的模板推导规则来实现完美转发。
我们来看个例子。我们知道,一个声明的右值引用其实是一个左值,这就为我们进行参数转发(传递)造成了问题:
#include <iostream>
void reference(int &v){
printf("左值\n");
}
void reference(int &&v){
printf("右值\n");
}
template <typename T>
void pass(T && v){
printf("普通传参:");
reference(v);
}
int main(){
printf("传递右值:");
pass(1); //1 是右值,输出左值
printf("传递左值:");
int l = 1;
pass(l); // l是左值,输出右值
return 0;
}
为什么会是这样的结果呢?
这是基于引用坍缩(折叠)规则(将复杂的未知表达式折叠为已知的简单表达式)的:
- 在传统C++中,我们不能对一个引用类型继续进行引用。也就是说:
typedef const int T;
typedef T& TR;
TR& v = 1; //C++11之前会编译错误
- 但是C++由于右值引用的出现而放宽了这一做法,从而产生了引用坍缩规则,允许我们对引用进行引用,即能左引用,又能右引用。但是遵循如下规则:
函数形参类型 | 实参函数类型 推到后函数形参类型 |
---|---|
T& | 左引用 |
T & | 右引用 |
T&& | 左引用 |
T&& | 右引用 |
因此,模板函数中使用T&&不一定能进行右值引用,当传入左值时,此函数的引用被推导为左值。更准确的讲,无论模板参数是什么类型的引用,当且仅当实参类型为右引用时,模板参数才能被推导为右引用类型。这才使得v作为左值的成功传递。
也就是说,我们可以把转发函数写成如下形式:
template<typename T>
void IamForwording(T &&t){
IrunCodeActually(static_cast<T &&>(t));
}
从而,如果我们调用转发函数时传入一个X类型的左值引用,转发函数将被实例化为如下形式:
void IamForwording(X& &&t){
IrunCodeActually(static_cast<X& &&>(t));
}
引用引入坍缩规则,就是:
void IamForwording(X& t){
IrunCodeActually(static_cast<X& >(t));
}
如果传入一个X类型的右值引用,转发函数将被实例化为如下形式:
void IamForwording(X&& &&t){
IrunCodeActually(static_cast<X&& &&>(t));
}
引用引入坍缩规则,就是:
void IamForwording(X&& t){
IrunCodeActually(static_cast<X&& >(t));
}
这里我们就看到了static_cast的重要性。
定义
完美转发就是基于引用坍缩(折叠)规则产生的。
- 所谓完美转发,就是为了让我们在传递参数的时候,保持原来的参数类型(左引用保持左引用,右引用保持右引用)。
- 为了解决这个问题,我们应该使用
std::forward
来进行参数的转发(传递):
#include <iostream>
void reference(int &v){
printf("左值\n");
}
void reference(int &&v){
printf("右值\n");
}
template <typename T>
void pass(T && v){
printf("\t普通传参:");
reference(v);
printf("\tstd::move传参:");
reference(std::move(v));
printf("\tstd::forward传参:");
reference(std::forward<T>(v));
printf("\tstd::static_cast传参:");
reference(static_cast<T&&>(v));
}
无论传递参数为左值还是右值,普通传参都会将参数作为左值转发,所以std::move
总是会接受到一个左值,从而转发调用了reference(int &&)
输出右值调用
唯有std::forward
既没有造成任何多余的拷贝,同时完美转发(传递)了函数的实参给了内部调用的其他函数
std::forward
和std::move一样。没有做任何事情,std::move单纯的将左值转化为右值,std::forward也只是单纯的将参数做了一个类型的转换,从现象上看,std::forward<T>v
和staatic_cast<T&&>v
是完全一样的。
那,为何一条语句能够针对两种类型的返回对应的值,我们来看以下std::forward的具体实现机制,std::forward包含两个重载:
template<typename _Tp>
constexpr _Tp&&forward(typename std::remove_reference<_Tp>::type& __t) noexcept{
return static_cast<_Tp&&>(__t);
}
template<typename _Tp>
constexpr _Tp&&forward(typename std::remove_reference<_Tp>::type&& __t)noexcept{
static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument substituting _Tp is an lvalue reference type");
return static_cast<_Tp&&>(__t);
}
在这份实现中,std::remove_reference的功能是消除类型中的引用,而std::is_lvalue_reference用于检查类型推导是否正确,在std::forward的第二个实现中检查了接收到的值确实是一个左值,进而体现了坍缩规则。
当std::forward接受左值时,_Tp被推导为左值,所以返回值为左值;而当其接受右值时,_Tp被推导为右值引用,则基于坍缩规则,返回值成了&& + &&的右值。可见std::forward的原理在于巧妙的利用了模板类型推导中产生的差异。
这也就能回答”为什么在使用循环语句的过程中,auto&& 是最安全的方式?“:因为当auto被推导为不同的左右引用时,与&&的坍缩组合是完美转发。