理解引用折叠
以下面这个模板为例
template<typename T>
void func(T&& param);
模板形参T的推导类型中,会把传给param
的实参是左值还是右值的信息给编码进去。
编码机制是直截了当的:如果传递的实参是个左值,T
的推导结果就是个左值引用类型;如果传递的实参是个右值,T
的推导结果就是非引用类型。
Widget widgetFactory(); //返回右值的函数
Widget w; //变量(左值)
func(w); //调用func并传入左值:T的推导结果类型为Widget&
func(WidgetFactory()) //调用func并传入右值:T的推导结果类型为Widget
两个对func
的调用,传递的实参类型都为Widget
。不同之处仅在于,一个是左值另一个是右值,而这个不同之处却导致了针对模板形参T
得出了不同的类型推导结果。
在C++中,引用的引用是非法的。
int x;
...
auto&& rx = x; //错误,不可以声明引用的引用
当左值被传递给接受万能引用的函数模板时会发生下面的状况
template<typename T>
void func(T&& param);
func(w); //调用func并传入左值:T的推导结果类型为Widget&
如果把T
的推导结果类型(即Widget&
)代码实例化模板。
void func(Widget& && param);
引用的引用!
引用折叠,我们被禁止声明引用的引用,但编译器却可以在特殊的语境中产生引用的引用,模板实例化就是这样的语境之一。
有两种引用(左值和右值),所以就有四种可能的引用——引用的组合(左值-左值,左值-右值,右值-左值,右值-右值)。如果引用的引用出现在允许的语境,该双重引用会折叠成单个引用,规则如下:
如果任一引用为左值引用,则结果为左值引用。否则(即两个皆为右值引用),结果为右值引用。
在上述例子中,将推导结果类型Widget&
带入函数模板func
后,产生了一个指向左值引用的右值引用,然后,根据引用折叠规则,结果是个左值引用。
引用折叠是使std::forward
得以运行的关键。
template<typename T>
void f(T&& fParam)
{
...
someFunc(std::forward<T>(fParam)); //将fParam转发至someFunc
}
由于fParam
是个万能引用,我们就知道,传递给f
的实参是左值还是右值的信息会被编码到类型形参T
中。std::forward
的任务是,当且仅当编码T
中的信息表明传递给实参是个右值,即T
的推导结果类型是个非引用类型时,对fParam
(左值)实施到右值的强制类型转换。
这里是std::forward
的一种能够完成任务的实现。
template<typename T>
T&& forward(typename remove_reference<T>::type& param)
{
return static_cast<T&&>(param);
}
假设传递给函数f
的实参的类型是个左值Widget
,则T
会被推导为Widget&
类型,然后对std::forward
的调用就会实例化为std::forward<Widget&>
,而将Widget&
插入std::forward
的实现就会产生如下结果。
Widget& && forward(typename
remove_reference<Widget&>::type& param)
{ return static_cast<Widget& &&>(param); }
由于类型特征remove_reference<Widget&>::type
的产生结果是Widget
类型,所以std::forward
又变换成了下面的结果。
Widget& && forward(Widget& param)
{ return static_cast<Widget& &&>(param); }
引用折叠同样在返回值和强制类型转换的语境中得到了实施,导致实际调用结果是这样的终极版本std::forward
。
Widget& forward(Widget& param)
{ return static_cast<Widget&>(param); }
如你所见,当左值实参被传递给函数模板f
时,std::forward
实例化结果是:接受一个左值引用,并返回一个左值引用,而std::forward
内部的强制类型转换未做任何事情。因为param
的类型已经是Widget&
,所以再要把它强制转换成Widget&
类型不会产生什么效果。综上,被传递给std::forward
的左值实参会返回一个左值引用。根据定义,左值引用是左值,所以传递给std::forward
会导致返回一个左值。
再假设传递给f
的实参是右值Widget
类型,在此情况下,f
的类型形参T
的推导结果是个光秃秃的Widget
。因此,f
内部的std::forward
就成了std::forward<Widget>
。在std::forward
的实现中,在T
之处用Widget
代入,就得出下面的代码:
Widget&& forward(typename remove_reference<Widget>::type& param)
{ return static_cast<Widget&&>(param); }
针对非引用Widget
类型实施std::remove_reference
会产生和起始类型相同的结果Widget
,所以std::forward
又变成了这样:
Widget&& forward(Widget& param)
{ return static_cast<Widget&&>(param); }
这里没有发生引用的引用,所以也就没有发生引用折叠,所以这也就已经是本次std::forward
调用的最终实例化版本了。
由函数返回的右值引用是定义为右值的,所以在此情况下,std::forward
会把f的形参fParam
(左值)转换成右值。最终的结果是,传递给函数f
的右值实参会作为右值转发给somefunc
函数。
在C++14
中有了std::remove_reference_t
,从而std::forward
的实现得以变得更加简明扼要。
template<typename T>
T&& forward(remove_reference_t<T>& param)
{
return static_cast<T&&>(param);
}
引用折叠会出现的语境有四种。第一种,模板实例化;第二种,是auto
变量的类型生成。
template<typename T>
void func(T&& param);
Widget widgetFactory(); //返回右值的函数
Widget w; //变量左值
func(w); //调用func并传入左值:T的推导结果类型为Widget&
func(WidgetFactory()) //调用func并传入右值:T的推导结果类型为Widget
这一切都能以auto
模仿。
下面这个声明:
auto&& w1 = w;
初始化w1
的是个左值,因此auto
的类型推导结果为Widget&
。在w1
声明中以Widget&
带入auto
,就产生了以下这段代码
Widget& && w1 = w;
引用折叠后,又会变成
Widget& w1 = w;
w1
仍然是左值引用。
下述声明
auto&& w2 = widgetFactory();
以右值初始化w2
,auto
的类型推导结果为非引用类型Widget
。将Widget
带入auto
就得到
Widget&& w2 = widgetFactory();
w2
是右值引用。
万能引用并非是一种新的引用类型,其实它就是满足了下面两个条件的语境中的右值引用;
- 类型推导的过程会区别左值和右值。
T
类型的左值推导结果为T&
,而T
类型的右值推导结果为T
- 会发生引用折叠
发生引用折叠的第三种语境是生成和使用typedef
和别名声明。
如果在typedef
的创建或者评估求值的过程中出现了引用的引用,引用折叠就会出手消灭它。
template<typename T>
class Widget{
public:
typename T&& RvalueRefToT;
...
};
假设我们以左值类型来实例化该Widget
。
Widget<int&> w;
在Widget
中以int&
代入T
的位置,则得到如下的typedef
:
typedef int& && RvalueRefToT;
引用折叠又将上述语句化简得到
typedef int& RvalueRefToT;
这个结果显然表明,我们为typedef
选择的名字也许有些名不副实:当以左值引用类型实例化Widget
时,RvalueRefToT
其实成了左值引用的typedef。
最后一种发生引用折叠的语境在于decltype
的运用中,如果在分析一个涉及decltype
的类型过程中出现了引用的引用,则引用折叠也会介入并消灭它。
要点速记
-
引用折叠会在四种语境中发生:模板实例化,auto类型生成,创建和运用typedef和别名声明,以及decltype。
- 万能引用就是在类型推导过程会区分左值和右值,以及会发生引用折叠的语境中的右值引用。
- 当编译器在引用折叠的语境下生成引用的引用时,结果会变成单个引用。如果原始的引用中有任一引用为左值引用,则结果为左值引用,否则,结果为右值引用。