参考:《深入理解C++11:新特性解析与应用》
所谓完美转发,指的是在函数模板中,完全依照模板的参数类型,将参数传递给函数模板中调用的另外一个函数。比如:
template <typename T>
void IamForwording(T t)
{
IrunCodeActually(t);
}
上面的 IamForwording 是一个转发函数模板,而函数 IrunCodeActually 则是真正执行代码的目标函数。
从函数 IrunCodeActually 的角度而言,总是希望转发函数将参数按照传入 IamForwording 时的类型传递(即传入 IamForwording 是左值对象, IrunCodeActually 就能获得左值对象,右值对象也是如此),而不产生额外的开销,就好像转发者不存在一样。
看起来似乎很容易,但是实际却不简单。上面例子中,IamForwording 的参数中使用了最基本类型进行转发,该方法会导致参数在传给 IrunCodeActually 之前就产生了一次额外的临时对象拷贝。所以这样的转发只能说是正确的转发,但谈不上完美。
通常程序员需要的是一个引用类型,引用类型不会有拷贝的开销,其次,则需要考虑转发函数对类型的接受能力。因为目标函数可能需要能够即接受左值引用,又接受右值引用。那么如果转发函数只能接受其中的一部分,我们也无法做到完美转发。
我们很容易想到“万能”的常量左值类型。不过以常量左值为参数的转发函数却会遇到一些尴尬,比如:
void IrunCodeActually(int t) {}
template <typename T>
void IamForwording(const T& t)
{
IrunCodeActually(t);
}
由于目标函数的参数类型是非常量左值引用类型,因此无法接受常量左值引用作为参数。虽然转发函数的接受能力很高,但在目标函数的接受上却出了问题。
我们可能需要通过一些常量和非常量的重载来解决目标函数的接受问题。但这在函数参数比较多的情况下,就会造成代码的冗余。如果我们的目标函数的参数是个右值引用,则同样无法接受任何左值类型作为参数,间接的,也就导致无法使用移动语义。
在 C++ 中是如何解决完美转发的问题呢?实际上,C++11 通过引入一条所谓的“引用折叠”的新语言规则,并结合新的模板推倒规则来完成完美转发。
在 C++11 之前,如下面的句子:
typedef const int T;
typedef T& TR;
TR& v = 1; //该声明在C++98中会导致编译错误
其中 TR& v = 1 这样的表达式会被编译器认为是不合法的表达式,而在 C++11 中,一旦出现了这样的表达式,就会发生引用折叠,即将复杂的未知表达式折叠为已知的简单表达式,折叠规则如下:
TR 的类型定义 | 声明 v 的类型 | v 的实际类型 |
---|---|---|
T& | TR | A& |
T& | TR& | A& |
T& | TR&& | A& |
T&& | TR | A&& |
T&& | TR& | A& |
T&& | TR&& | A&& |
可以看出一个规则:一旦定义中出现了左值引用,引用折叠总是优先将其折叠为左值引用。
而模板对类型的推倒规则就比较简单,当转发函数的实参是类型 X 的一个左值引用,那么模板参数被推倒为 X& 类型;而转发函数的实参是类型 X 的一个右值引用的话,那么模板的参数将被推导为 X&& 类型,结合上面的引用折叠规则,就能确定出参数的实际类型。进一步,就可以把转发函数写成如下形式:
void IamForwording(X& && T)
{
IrunCodeActually(static_case<X& &&>(t));
}
应用之前的引用折叠规则,就是:
void IamForwording(X& t)
{
IrunCodeActually(static_case<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(std::forward(t));
}
std::move 和 std::forward 在实际上的差别并不大。不过标准库这么设计,也许是为了让每个名字对应不同的用途,以应对未来可能的拓展(虽然现在使用 move 可能也能通过完美转发函数的编译,但并不推荐)。
接下来看一个 std::forward 完美转发的例子:
#include
void RunCode(int &&m) { std::cout << "RValue Ref" << std::endl; }
void RunCode(int &m) { std::cout << "LValue Ref" << std::endl; }
void RunCode(const int &&m) { std::cout << "Const RValue Ref" << std::endl; }
void RunCode(const int &m) { std::cout << "Const LValue Ref" << std::endl; }
template <typename T>
void PerfectForward(T &&t)
{
RunCode(std::forward<T>(t));
}
int main(int argc, char **argv)
{
int a;
int b;
const int c = 1;
const int d = 0;
PerfectForward(a);
PerfectForward(std::move(b));
PerfectForward(c);
PerfectForward(std::move(d));
return 0;
}
运行结果:
LValue Ref
RValue Ref
Const LValue Ref
Const RValue Ref
可以看到所有的 4 种类型的值对完美转发进行测试,所有的转发都被正确的送到了目的地。
完美转发的一个作用就是做包装函数,这是一个很方便的功能。对代码中的完美转发函数稍作修改,就可以用很少的代码记录单参数函数的参数传递状况,代码如下:
#include
template <typename T, typename U>
void PerfectForward(T &&t, U &Func)
{
std::cout << t << "\tforwarded..." << std::endl;
Func(std::forward<T>(t));
}
void RunCode(double &&m) {}
void RunHome(double &&h) {}
void RunComp(double &&c) {}
int main(int argc, char **argv)
{
PerfectForward(1.5, RunComp);
PerfectForward(8, RunCode);
PerfectForward(1.5, RunHome);
}
运行结果:
1.5 forwarded...
8 forwarded...
1.5 forwarded...
这只是个简单的例子,可以尝试变得更复杂,以更加符合实际需求。事实上,在 C++11 标准库中,我们可以看到大量的完美转发的实际应用,一些小巧好用的函数,比如:make_pair、make_unique 等在 C++11 中都通过完美转发实现了。这样一来,就减少了一些函数版本的重复(const 和非 const 版本的重复),并能够充分利用移动语义。无论从运行性能的提高还是从代码编写的简化上,完美转发都堪称完美。