Effective Modern C++ 条款28 理解引用折叠
理解引用折叠
条款23提起过把一个实参传递给模板函数时,无论实参是左值还是右值,推断出来的模板参数都会含有编码信息。那条款没有提起,只有模板形参是通用引用时,这件事才会发生,不过对于这疏忽,理由很充分:条款24才介绍通用引用。把这些关于通用引用和左值/右值编码信息综合,意味着这个模板,
template void func(t&& param);
无论param是个左值还是右值,需要推断的模板参数t都会被编码。
编码技术是很简单的,当传递的实参是个左值时,t就被推断为一个左值引用,当传递的实参是个右值时,t就被推断为一个非引用类型。(注意这是不对称的:左值会编码为左值引用,但右值编码为非引用。)因此:
widget widgetfactory(); // 返回右值的函数 widget w; // 一个变量,左值 func(w); // 用左值调用函数,t被推断为widget& func(widgetfactory()); // 用右值调用函数,t被推断为widget
两个func调用都是用widget参数,不过一个widget是左值,另一个是右值,从而导致了模板参数t被推断出不同的类型。这,正如我们将很快看到,是什么决定通用引用变成左值引用或右值引用的,而这也是std::forward完成工作所使用的内部技术。
在我们紧密关注std::forward和通用引用之前,我们必须注意到,在c++中对引用进行引用是不合法的。你可以尝试声明一个,你的编译器会严厉谴责加*:
int x; ... auto& & rx = x; // 报错!不可以声明对引用的引用
但想一想当一个左值被传递给接受通用引用的模板函数时:
template void func(t&& param); // 如前 func(w); // 用左值调用func,t被推断为widget&
如果使用推断出来的t类型(即widget&)实例化模板,我们得到这个:
void func(widget& && param);
一个对引用的引用!然而你的编译器内有深刻谴责加*。我们从条款24知道,通用引用
param用一个左值进行初始化,
param的类型应该出一个左值引用,但编译器是如何推断t的类型的,还有是怎样把它替代成下面这个样子,哪一个才是最终的签名呢?
void func(widget& param);
答案是引用折叠。是的,你是禁止声明对引用的引用,但编译器在特殊的上下文中可以产生它们,模板实例化就是其中之一。当编译器生成对引用的引用时,引用折叠指令就会随后执行。
有两种类型的引用(左值和右值),所以有4种可能的对引用引用的组合(左值对左值,左值对右值,右值对左值,右值对右值)。如果对引用的引用出现在被允许的上下文(例如,在模板实例化时),这个引用(即引用的引用,两个引用)会折叠成一个引用,根据的是下面的规则:
如果两个引用中有一个是左值引用,那么折叠的结果是一个左值引用。否则(即两个都是右值引用),折叠的结果是一个右值引用。
在我们上面的例子中,在函数func中把推断出来的类型widget&替代t后,产生了一个对右值的左值引用,然后引用折叠规则告诉我们结果是个左值引用。
引用折叠是使std::forward工作的关键部分。就如条款25解释那样,对通用引用使用std::forward,是一种常见的情况,像这样:
template void f(t&& fparam) { ... // do some works somefunc(std::forward(fparam)); // 把fparam转发到somefunc }
因为
fparam是一个通用引用,我们知道无论传递给函数f的实参(即用来初始化fparam的表达式)是左值还是右值,参数类型t都会被编码。std::forward的工作是,当且仅当传递给函数f的实参是个右值时,把
fparam(左值)转换成一个右值。
这里是如何实现std::forward来完成工作:
template // 在命名空间std中 t&& forward(typename remove_reference::type& param) { return static_cast(param); }
这没有完全顺应标准库(我省略了一些接口细节),不过不同的部分是与理解std::forward如何工作无关。
假如传递给函数f的是个左值的widget,t会被推断为widget&,然后调用std::forward会让它实例化为
std::forward
widget& && forward(typename remove_reference::type& param) { return static_cast(param); }
remove_reference
widget& && forward(widget& param) { return static_cast(param); }
在返回类型和cast中都会发生引用折叠,导致被调用的最终版本的std::forward:
widget& forward(widget& param) { return static_cast(param); }
就如你所见,当一个左值被传递给模板函数f时,std::forward被实例化为接受一个左值引用和返回一个左值引用。std::forward内部的显式转换没有做任何东西,因为
param的类型已经是widget&了,所以这次转换没造成任何影响。一个左值实参被传递给std::forward,将会返回一个左值引用。根据定义,左值引用是左值,所以传递一个左值给std::forward,会导致std::forward返回一个左值,就跟它应该做的那样。
现在假设传递给函数f的是个右值的widget。在这种情况下,函数f的类型参数t会被推断为widget。因此f里面的std::forward会变成
std::forward
widget&& forward(typename remove_reference::type& param) { return static_cast(param); }
对非引用widget使用std::remove_reference会产生原来的类型(widget),所以std::forward变成这样:
widget&& forward(widget& param) { return static_cast(param); }
这里没有对引用的引用,所以没有进行引用折叠,这也就这次std::forward调用的最终实例化版本。
由函数返回的右值引用被定义为右值,所以在这种情况下,std::forward会把f的参数
fparam(一个左值)转换成一个右值。最终结果是传递给函数f的右值实参作为右值被转发到somefunc函数,这是顺理成章的事情。
在c++14中,std::remove_reference_t的存在可以让std::forward的实现变得更简洁:
template // c++14,在命名空间std中 t&& forward(remove_reference_t& param) { return static_cast(param); }
引用折叠出现在四种上下文。第一种是最常见的,就是模板实例化。第二种是auto变量的类型生成。它的细节本质上和模板实例化相同,因为auto变量的类型推断和模板类型推断本质上相同(看条款2)。再次看回之前的一个例子:
template void func(t&& param); widget widgetfactory(); // 返回右值的函数 widget w; // 一个变量,左值 func(w); // 用左值调用函数,t被推断为widget& func(widgetfactory()); // 用右值调用函数,t被推断为widget
这可以用auto形式模仿。这声明
auto&& w1 = w;
用个左值初始化w1,因此auto被推断为widget&。在声明中用widget&代替auto声明w1,产生这个对引用进行引用的代码,
widget& && w1 = w;
这在引用折叠之后,变成
widget& w1 = w;
结果是,w1是个左值引用。
另一方面,这个声明
auto&&w2 = widgetfactory();
用个右值初始化w2,导致auto被推断为无引用类型widget,然后用widget替代auto变成这样:
widget&& w2 = widgetfactory();
这里没有对引用的引用,所以我们已经完成了,w2是个右值引用。
我们现在处于真正能理解条款24介绍通用引用的位置了。通用引用不是一种新的引用类型,实际上它是右值引用——在满足了下面两个条件的上下文中:
根据左值和右值来进行类型推断。t类型的左值使t被推断为t&,t类型的右值使t被推断为t。 发生引用折叠
通用引用的概念是很有用的,因为它让你免受:识别出存在引用折叠的上下文,弱智地根据左值和右值推断上下文,然后弱智地把推断出的类型代进上下文,最后使用引用折叠规则。
我说过有4中这样的上下文,不过我们只讨论了两种:模板实例化和auto类型生成。第三种上下文就是使用typedef和类型别名声明(看条款9)。如果,在typedef创建或者评估期间,出现了对引用的引用,引用折叠会出面消除它们。例如,假如我们有个类模板widget,内部嵌有一个右值引用类型的typedef,
template class widget { public: typedef t&& rvaluereftot; ... };
然后假如我们用一个左值引用来实例化widget:
widget w;
在widget中用int&代替t,typedef变成这样:
typedef int& && rvaluereftot;
引用折叠把代码弄出这样:
type int& rvaluereftot;
这很明显的告诉我们,我们typedef选择的名字跟我们期望得到的不一样:当用左值引用实例化widget时,rvaluereftot是个左值引用的typedef。
最后的一种会发生引用折叠的上下文是使用decltype中。如果,在分析一个使用decltype的类型期间,出现了对引用的引用,引用折叠会出面消除它。(关于decltype的详细信息,请看条款3。)
总结
总结
需要记住的3点:
引用折叠会出现在4中上下文:模板实例化,auto类型生成,typedef和类型别名声明的创建和使用,decltype。 当编译器在一个引用折叠上下文中生成了对引用的引用时,结果会变成一个引用。如果原来的引用中有一个是左值引用,结果就是个左值引用。否则,结果是个右值引用。 通用引用是——出现在类型推断区分左值和右值和出现引用折叠的上下文中的——右值引用。
上一篇: MySQL运算符的使用分析
下一篇: ASP.NET MVC 域名泛解析设置