-
本节中,我们来讨论“万能引用”这种抽象的本质,以及
std::forward
的实现机理。 -
Item 23和24中说明了当使用万能引用参数
T&&
时,入参的类型信息会被记录在T
中。规则实际上很简单:当入参为左值时,T
(注意,不是T&&
)被推导为左值引用;当入参为右值时,T
被推导为非引用(注意这里的不对称性)。以一个例子来说明:
template<typename T>
void func(T&& param);
Widget widgetFactory(); // function returning rvalue
Widget w; // a variable (an lvalue)
func(w); // call func with lvalue; T deduced to be Widget&
func(widgetFactory()); // call func with rvalue; T deduced to be Widget
- 我们需要了解的另一个规则是,C++中不允许出现对引用的引用。但是由以上规则,如果万能引用接收的入参是左值,那么经实例化的函数
func
的签名似乎应该为:
void func(Widget& && param);
明显违反了这条规则。这就涉及了本节的主题引用折叠(reference collapsing):尽管你不能声明对引用的引用,但编译器在一些情景中(模板实例化就是一种)可以产生它们。我们知道,C++中有左值和右值引用,于是可能出现四种对引用的引用的组合:左值-左值,左值-右值,右值-左值,右值-右值。接下来,编译器会将这些引用根据以下规则“塌缩”为一层引用:只要两层引用中有任意一个是左值引用,那么结果就是左值引用,否则结果是右值引用。也就是前三种情形会被转换为左值引用,第四种会被转换为右值引用。
在上面的例子中,入参为左值引用的情况经折叠后得到:
void func(Widget& param);
至于入参为右值引用的情况,根据上面所述 T
会被推导为 Widget
,则实例化结果直接为:
void func(Widget&& param);
恰好为我们所预期的万能引用的表现:入参是什么引用就实例化为什么引用。
-
至此你可能已经意识到,所谓的“万能引用”,其实不是一种新的引用形式,而是对右值引用在发生了类型推导和引用折叠情况下的表现的概括。
-
引用折叠总共会发生在四种情况中。以上说明的模板实例化是最常见的一种。第二种类似的是
auto
变量类型的推导,仍以一个例子说明:
Widget widgetFactory(); // function returning rvalue
Widget w; // a variable (an lvalue)
auto&& w1 = w;
// w1以左值w初始化,auto被替换为Widget&,得到以下形式
Widget& && w1 = w;
// 经引用折叠,得到w1的最终类型
Widget& w1 = w;
auto&& w2 = widgetFactory();
// w2以右值初始化,auto被替换为Widget,直接得到w2的类型
Widget&& w2 = widgetFactory();
第三种情况是 typedef
和别名声明(alias declaration)的生成。引用折叠会消灭在推断一个 typedef
时出现的对引用的引用。例如有以下模板类:
template<typename T>
class Widget {
public:
typedef T&& RvalueRefToT;
...
};
如果我们用左值引用类型实例化 Widget
类:
Widget<int&> w;
那么 RvalueRefToT
的推断中会触发折叠:
typedef int& && RvalueRefToT;
↓
typedef int& RvalueRefToT; // 看起来RvalueRefToT的名称不太合适
第四种情况是使用 decltype
时。如果分析 decltype(v)
中 v
的类型时出现了引用的引用,也会触发引用折叠。
- 了解了引用折叠的机理,就可以理解
std::forward
是如何实现的了。std::forward
的基本代码为(省略了一些与此处无关的细节):
template <class T>
T&& forward(remove_reference_t<T>& param) {
return static_cast<T&&>(param);
}
以对万能引用参数调用 std::forward
为例, 当入参为 Widget
的左值时,根据上面的讨论可知 T
被推导为 Widget&
,于是调用形式为 std::forward<Widget&>(param)
,替换并经折叠得到:
Widget& && forward(remove_reference_t<Widget&>& param)
{ return static_cast<Widget& &&>(param); }
↓
Widget& forward(Widget& param)
{ return static_cast<Widget&>(param); }
当入参为 Widget
的右值时,param
仍是左值,但 T
被推导为 Widget
,于是调用形式为 std::forward<Widget>(param)
,替换(不发生折叠)得到:
Widget&& forward(remove_reference_t<Widget>& param)
{ return static_cast<Widget&&>(param); }
↓
Widget&& forward(Widget& param)
{ return static_cast<Widget&&>(param); }
可以看出,两种情况下虽然入参 param
表面上都为左值,但通过附带的 T
的类型信息,实现了对 param
“真实类别”的识别,当其为右值时才进行类型转换,左值则什么都不做,这正是 Item 23 中讲到的 std::forward<T>()
的选择性类型转换功能。
有兴趣的读者如果阅读STL源码会发现
std::forward
还有另一种重载形式,其参数类型为右值(remove_reference_t<T>&&
)。本例对应的完美转发情形中不会调用该重载,至于其为什么存在,可以参考这个链接的回答。
总结
- 引用折叠发生在四种情况中:模板实例化,
auto
类型生成,typedef
和 alias declaration 的创建和使用,decltype
。 - 当编译器生成了对引用的引用时,会根据引用折叠规则将其转换为一层引用。只要两层引用中有一个是左值引用,结果就是左值引用,否则是右值引用。
- 万能引用的本质是右值引用在发生类型推导和引用折叠现象时的表现的概括。