3.3.6 完美转发

所谓完美转发(perfect forwarding),是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。比如:


template<typename T>

void IamForwording(T t){IrunCodeActually(t);}


这个简单的例子中,IamForwording是一个转发函数模板。而函数IrunCodeActually则是真正执行代码的目标函数。对于目标函数IrunCodeActually而言,它总是希望转发函数将参数按照传入Iamforwarding时的类型传递(即传入IamForwording的是左值对象,IrunCodeActually就能获得左值对象,传入IamForwording的是右值对象,IrunCodeActually就能获得右值对象),而不产生额外的开销,就好像转发者不存在一样。

这似乎是一件非常容易的事情,但实际却并不简单。在上面例子中,我在IamForwording的参数中使用了最基本类型进行转发,该方法会导致参数在传给IrunCodeActually之前就产生了一次额外的临时对象拷贝。因此这样的转发只能说是正确的转发,但谈不上完美。

所以通常程序员需要的是一个引用类型,引用类型不会有拷贝的开销。其次,则需要考虑转发函数对类型的接受能力。因为目标函数可能需要能够既接受左值引用,又接受右值引用。那么如果转发函数只能接受其中的一部分,我们也无法做到完美转发。结合表3-1,我们会想到“万能”的常量左值类型。不过以常量左值为参数的转发函数却会遇到一些尴尬,比如:


void IrunCodeActually(int t){}

template<typename T>

void IamForwording(const T&t){IrunCodeActually(t);}


这里,由于目标函数的参数类型是非常量左值引用类型,因此无法接受常量左值引用作为参数,这样一来,虽然转发函数的接受能力很高,但在目标函数的接受上却出了问题。那么我们可能就需要通过一些常量和非常量的重载来解决目标函数的接受问题。这在函数参数比较多的情况下,就会造成代码的冗余。而且依据表3-1,如果我们的目标函数的参数是个右值引用的话,同样无法接受任何左值类型作为参数,间接地,也就导致无法使用移动语义。

那C++11是如何解决完美转发的问题的呢?实际上,C++11是通过引入一条所谓“引用折叠”(reference collapsing)的新语言规则,并结合新的模板推导规则来完成完美转发。

在C++11以前,形如下列语句:


typedef const int T;

typedef T&TR;

TR&v=1;//该声明在C++98中会导致编译错误


其中TR&v=1这样的表达式会被编译器认为是不合法的表达式,而在C++11中,一旦出现了这样的表达式,就会发生引用折叠,即将复杂的未知表达式折叠为已知的简单表达式,具体如表3-2所示。

3.3.6 完美转发 - 图1

这个规则并不难记忆,因为一旦定义中出现了左值引用,引用折叠总是优先将其折叠为左值引用。而模板对类型的推导规则就比较简单,当转发函数的实参是类型X的一个左值引用,那么模板参数被推导为X&类型,而转发函数的实参是类型X的一个右值引用的话,那么模板的参数被推导为X&&类型。结合以上的引用折叠规则,就能确定出参数的实际类型。进一步,我们可以把转发函数写成如下形式:


template<typename T>

void IamForwording(T&&t){

IrunCodeActually(static_cast<T&&>(t));

}


注意 对于完美转发而言,右值引用并非“天生神力”,只是C++11新引入了右值,因此为其新定下了引用折叠的规则,以满足完美转发的需求。

注意一下,我们不仅在参数部分使用了T&&这样的标识,在目标函数传参的强制类型转换中也使用了这样的形式。比如我们调用转发函数时传入了一个X类型的左值引用,可以想象,转发函数将被实例化为如下形式:


void IamForwording(X&&&t){

IrunCodeActually(static_cast<X&&&>(t));

}


应用上引用折叠规则,就是:


void IamForwording(X&t){

IrunCodeActually(static_cast<X&>(t));

}


这样一来,我们的左值传递就毫无问题了。实际使用的时候,IrunCodeActually如果接受左值引用的话,就可以直接调用转发函数。不过读者可能发现,这里调用前的static_cast没有什么作用。事实上,这里的static_cast是留给传递右值用的。

而如果我们调用转发函数时传入了一个X类型的右值引用的话,我们的转发函数将被实例化为:


void IamForwording(X&&&&t){

IrunCodeActually(static_cast<X&&&&>(t));

}


应用上引用折叠规则,就是:


void IamForwording(X&&t){

IrunCodeActually(static_cast<X&&>(t));

}


这里我们就看到了static_cast的重要性。如我们在上面几个小节中讲到的,对于一个右值而言,当它使用右值引用表达式引用的时候,该右值引用却是个不折不扣的左值,那么我们想在函数调用中继续传递右值,就需要使用std::move来进行左右值的转换。而std::move通常就是一个static_cast。不过在C++11中,用于完美转发的函数却不再叫作move,而是另外一个名字:forward。所以我们可以把转发函数写成这样:


template<typename T>

void IamForwording(T&&t){

IrunCodeActually(forward(t));

}


move和forward在实际实现上差别并不大。不过标准库这么设计,也许是为了让每个名字对应于不同的用途,以应对未来可能的扩展(虽然现在我们使用move可能也能通过完美转发函数的编译,但这并不是推荐的做法)。

我们来看一个完美转发的例子,如代码清单3-24所示。

代码清单3-24


include <iostream>

using namespace std;

void RunCode(int&&m){cout<<"rvalue ref"<<endl;}

void RunCode(int&m){cout<<"lvalue ref"<<endl;}

void RunCode(const int&&m){cout<<"const rvalue ref"<<endl;}

void RunCode(const int&m){cout<<"const lvalue ref"<<endl;}

template<typename T>

void PerfectForward(T&&t){RunCode(forward<T>(t));}

int main(){

int a;

int b;

const int c=1;

const int d=0;

PerfectForward(a);//lvalue ref

PerfectForward(move(b));//rvalue ref

PerfectForward(c);//const lvalue ref

PerfectForward(move(d));//const rvalue ref

}

//编译选项:g++ -std=c++11 3-3-9.cpp


在代码清单3-24中,我们使用了表3-1中的所有4种类型的值对完美转发进行测试,可以看到,所有的转发都被正确地送到了目的地。

完美转发的一个作用就是做包装函数,这是一个很方便的功能。我们对代码清单3-24中的转发函数稍作修改,就可以用很少的代码记录单参数函数的参数传递状况,如代码清单3-25所示。

代码清单3-25


include <iostream>

using namespace std;

template<typename T,typename U>

void PerfectForward(T&&t,U&Func){

cout<<t<<"\tforwarded…"<<endl;

Func(forward<T>(t));

}

void RunCode(double&&m){}

void RunHome(double&&h){}

void RunComp(double&&c){}

int main(){

PerfectForward(1.5,RunComp);//1.5 forwarded…

PerfectForward(8,RunCode);//8 forwarded…

PerfectForward(1.5,RunHome);//1.5 forwarded…

}

//编译选项:g++ -std=c++11 3-3-10.cpp


当然,读者可以尝试将该例子变得更复杂一点,以更加符合实际的需求。事实上,在C++11标准库中我们可以看到大量完美转发的实际应用,一些很小巧好用的函数,比如make_pair、make_unique等在C++11都通过完美转发实现了。这样一来,就减少了一些函数版本的重复(const和非const版本的重复),并能够充分利用移动语义。无论从运行性能的提高还是从代码编写的简化上,完美转发都堪称完美。