在我们日常的编程生活中,可能会遇到这样的情形:在一个函数中调用另一个函数。你真的真的是认真的吗?这个常见到几乎不想见了。我的意思是把这个函数的参数原封不动地传递给另一个函数(什么叫原封不动?就是保留参数类型的所有信息,更准确地说如果参数类型是左值引用,那传递的还是左值引用,右值引用也是如此。上文我们知道了类型是右值引用的参数,其表达式类型还是个左值引用)。
void g(MyClass& lrc) {
std::cout << "g(MyClass& c)\n";
}
void g(const MyClass& lrc) {
std::cout << "g(const MyClass& lrc)\n";
}
void g(MyClass&& rrc) {
std::cout << "g(MyClass&& c)\n";
}
为了能够调用上面三种不同的参数类型的g函数,我定义了三个相应的同参数类型的f函数。
void f(MyClass& val) {
g(val);
}
void f(const MyClass& val) {
g(val);
}
void f(MyClass&& val) {
g(val);
}
f函数的功能就是把调用参数转发给g函数(调用之前你也可以做些日志记录或者参数验证之类的工作),调用的版本一一对应,都会找到相应参数类型的那个版本。但是f的定义显得很boiling,如果将来g函数的定义有调整,可能会影响到f函数的定义,这种绑定修改非常讨厌。有没有什么好的办法来替代呢?
C++11给出了它的解决方案:参数的完美转发(perfect forwarding)。
template<typename T>
void f(T&& val) {
g(std::forward<T>(val));
}
这里有一个新的东西,尽管它看起来很像一个旧的东西。T&&,这个并不是右值引用,它的名称叫转发引用(Meyers称它为万能引用,因为它可以引用任何东西。由于它的出现主要是用于参数的原样转发,还是按照C++标准称呼转发引用更为恰当)。
当f被调用时,T的类型由编译器推导得出(调用时存在类型推导是它区别于右值引用的一个关键),推导结果可能是左值引用,也可以是右值,同时还可以保留参数类型的const或volatile属性。不过,并不是说只要涉及类型推导就是转发引用。
template<typename T>
void f(std::vector<T>&& val) {
g(std::forward<T>(val));
}
这里也有类型推导,但是std::vector<T>&&是个右值引用。转发引用的形式只能是T&&,甚至const T&&都会被为看成是右值引用(这点跟它的本意很类似,必须原封不动,不能有任何形式的添加)。
好了,现在可以看看std::forward<T>()到底干了什么了。就像std::move()并没有移动任何东西一样,std::forward<T>()也没有转发给目标函数任何东西。这哥俩儿有点像,都只是一个强制转换工具,不同的是std::move是无条件转换为右值引用,而std::forward的转换是有条件的,只在T推导出来的类型为右值时才会强制转换为右值引用(再次强调:形参总是左值,即使它的类型是右值引用)。
对于左值引用,编译器会通过一项引用折叠的技术(将多重引用折叠成单个引用),去除多余的引用,最终还是一个左值引用。
STL里面大量使用了转发引用,特别是结合变长模板参数,例如std::make_shared这样的便携函数,还有就是容器类中的emplace方法。如果你是一个通用模板库的开发者,那转发引用会是一个短小而威力巨大的工具。
简单总结一下,转发引用会让编译器完成类型的推导,推导的过程中会保留参数的左值和右值的类型信息。这样如果参数是右值引用的话,通过使用std::forward可以将参数继续以右值引用的身份投入到新的函数怀抱。对于左值的情形,std::forward并不会改变它的类型。