Effective Modern C++ Item 28 理解引用折叠

本文围绕C++展开,介绍了引用折叠和std::forward。实参传递给函数模板时,模板形参推导会编码实参左右值信息。引用折叠可解决‘引用的引用’问题,有四种语境。std::forward能根据实参左右值情况进行型别转换,传入左值和右值时推导结果不同。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Item 23 曾经提到,实参在传递给函数模板时,推导出来的模板形参会将实参是左值还是右值的信息编码到结果型别中。但此条款未曾提到,这个编码操作只有在实参被用用以初始化的形参为万能引用时,才会发生。不过,有一个充分的理由来解释为什么当时不提及这些:万能引用是在Item24中才介绍。把万能引用和左右值编码信息的论述综合起来,意思就是,按照下面这个模板为例:

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得到了不同的型别推导结果。这个机制,正如我们将很快看到的,这就是决定了万能引用是变成左值引用还是右值引用的机制,也是std::foward得以运作的机制。

什么是引用折叠

在我们以更深入的视角来观察std::foward和万能引用之前,我们必须了解一个事实,在C++中,“引用的引用”是非法的。如果你敢尝试申明一个,则编译器会报错:

int x;
...
auto& & rx = x;     //错误!不可以声明“引用的引用”!

但如果仔细琢磨一下, 当左值被传递给接受万能引用的函数模板时,会发生下面的状况:

template<typename T>
void func(T&& param);       //同前
func(w);                    //调用func并传入左值:T的推导结果型别为Widget &

如果把T的推导结果型别(即Widget&)代码实例化模板,不就得到下面的结果了吗?

void func(Widget& && param);

引用的引用!然而编译器却不报错。Item24告诉过我们,由于万能引用param是用左值初始化的,其推导结果型别理应是个左值引用,但编译器是如何取用了T的推导结果型别的,并代入模板,从而使它拥有了下面这个终极版函数签名的呢?

void func(Widget& param);

答案就是引用折叠。是的,你是被禁止声明引用的引用,但编译器却可以在特殊的语境中产生引用的引用。模板实例化就是这样特殊的语境之一。当编译器生成引用的引用时,引用折叠机制变支配了接下来发生的事情。

有两种引用(左值和右值),所以就有四种可能的引用——引用组合(左值-左值,左值-右值,右值-左值,右值-右值)。如果引用的引用出现在允许的语境(例如,在模板实例化过程中),该双重引用会折叠成单个引用,规则如下:

如果任一引用为左值 引用,则结果为左值引用。否则(即两个皆为右值引用),结果为右值引用。

在上述例子中,将推导结果型别Widget&代入函数模板func后,产生了一个指涉到左值引用的右值引用。然后,根据引用折叠规则,结果是个左值引用。

std::forward 详解

引用折叠是使std::forward得以运作的关键。如同 Item 25 所解释的那样,std::forward会针对万能引用实施,这么一来,就会出现如下的常见用例:

template<typename T>
void f(T&& fParam)
{
    ...                                 //完成一些操作

    someFunc(std::forward<T>(fParam));  //将fParam转发至someFunc
}

由于fParam是个万能引用,我们就知道,传递给f的实参(即用以初始化fParam的表达式)是左值还是右值的信息会被编码到型别形参T中。std::forward的任务是当且仅当编码T中的信息表明传递给实参的是个右值,即T的推导结果型别是个非引用型别时,对fParam(左值)实施到右值的强制型别转换。

以下,是std::forward的一个能够完成任务的实现:

template<typenaem T>                                //在namespace std中
T&& forward(typename 
                remove_refernece<T>::type& param)
{
    return static_cast<T&&>(param);
}

这个实现和标准并不完全符合(省去了若干接口细节),不过,那些有差异的部分对于理解std::forward的行为并无干系。

std::forward 传入左值时的推导

假设传递给函数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_renference<Widget&>::type的产生结果是Widget型别,详见Item 9 ,所以std::forward又变成了下面的样子:

Widget& && forward(Widget & param)
{
    return static_cast<Widget& &&>(param);
}

引用折叠效果同样在返回值和强制类型转换的语境下,得到了实施,导致实际调用结果是这样的终极版本std::forward

Widget& forward(Widget& param)                  //在namespace std中
{
    return static_cast<Widget&>(param);
}

如你所见,当左值实参被传递给函数模板f时,std::forward实例化结果是:接受一个左值引用,并返回一个左值引用。而std::forward内部的强制型别转换未做任何事,因为param的型别已经是Widget&了,所以再把它强制转换到Widget&型别不会产生什么效果。

综上,被传递给std::forward的左值实参会返回一个左值引用。根据定义,左值引用时左值,所以传递左值给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_renference会产生和起始型别相同的结果(Widget),所以std::forward又成了下面这样:

Widget&& forward(Widget& param)
{
    return static_cast<Widget&&>(param);
}

这里没有发生引用的引用,所以也就没有发生引用折叠,所以这也就已经是本次std::forward调用的最终实例化版本了。

由函数返回的右值引用是定义为右值的,所以在此情况下,std::forward会把f的形参fParam(左值)转换为右值。最终的结果是,传递给函数f的右值实参会作为右值转发到someFunc函数,这也精确地符合认知。

在C++14中,有了std::remove_renference_t,从而std::forward的实现的以变得更加简明扼要:

template<typename T>
T&& forward(remove_renference_t<T>& param)
{
    return static_cast<T&&>(param);
}

引用折叠语境的四种情况

引用折叠会出现的语境有四种:

第一种,最常见的一种就是模板实例化。

第二种,是auto变量的型别生成。

技术细节本质上和模板实例化一模一样,因为auto变量的型别推导和模板的型别推导在本质上就是一模一样的(Item2)。重新反思下本条款介绍前面出现过的一个例子:

template<typename T>
void func(T&& param);

Widget widgetFactory();             //返回右值的甘薯

Widget w;                           //变量(左值)

func(w);                        //以左值调用函数,T的推导结果型别为Widget &

func(widgetFactory())           //以右值调用函数,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乃是右值引用。

话说到这里,我们才真正地理解了Item24中介绍的万能引用。万能引用并非一种新的引用型别,其实它就是满足了下面两个条件的语境中的右值引用:

  • 型别推导的过程会区别左值和右值。T型别的左值推导结果为T&,而T型别的右值则推导结果为T。

  • 会发横引用折叠

万能引用的概念是有用的,有了这个概念以后,就避免了需要识别出存在引用折叠的语境,根据左值和右值的不同脑补推导过程,然后在脑补针对推导的结果型别代入引用折叠发生的语境后应用引用折叠规则。

第三种语境是生成和使用typedef和别名声明(Item9)。

如果在typedef的创建或者评估求值的过程中出现了引用的引用,引用折叠就会起到作用。

例如,假设我们有个类模板Widget,内嵌一个右值引用型别的typedef。

template<typename T>
class Widget {
public:
    typedef T && RvalueRefToT;
....
};

再假设我们以左值引用型别来实例化该Widget:

Widget<int&> w;

Widget中以int&代入T的位置,则得到如下的typedef:

typedef int& && RvalueRefToT;

引用折叠又将上述语句简化得到:

typedef int& RvalueRefToT;

这个结果显然表明,我们为typedef选择的名字也许有些名不副实:当以左值引用型别实例化Widget时,RvalueRefToT其实成了个左值引用的typedef。

第四种会发生引用折叠的语境在decltype的运用中。

如果分析一个涉及decltype的型别过程中出现了引用的引用,则引用折叠亦会介入。(Item3)

要点速记
1. 引用折叠会在四种语境中发生:模板实例化,auto型别生成,创建和运用typedef和别名声明,以及decltype。
2. 当编译器在引用折叠的语境下生成引用的引用时,结果会变成单个引用。如果原始的引用中有任一引用为左值引用,则结果为左值引用。否则,结果为右值引用。
3. 万能引用就是在型别他UI到的过程中会区别左值和右值,以及会发生引用折叠的语境中的右值引用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值