Effective Modern C++ 条款28 理解引用折叠

理解引用折叠

条款23提起过把一个实参传递给模板函数时,无论实参是左值还是右值,推断出来的模板参数都会含有编码信息。那条款没有提起,只有模板形参是通用引用时,这件事才会发生,不过对于这疏忽,理由很充分:条款24才介绍通用引用。把这些关于通用引用和左值/右值编码信息综合,意味着这个模板,

template<typename T>
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<typename T>
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<typename T>
void f(T&& fParam)
{
    ...        // do some works

    someFunc(std::forward<T>(fParam));   // 把fParam转发到someFunc
}

因为fParam是一个通用引用,我们知道无论传递给函数f的实参(即用来初始化fParam的表达式)是左值还是右值,参数类型T都会被编码。std::forward的工作是,当且仅当传递给函数f的实参是个右值时,把fParam(左值)转换成一个右值。

这里是如何实现std::forward来完成工作:

template<typename T>          // 在命名空间std中
T&& forward(typename remove_reference<T>::type& param)
{
    return static_cast<T&&>(param);
}

这没有完全顺应标准库(我省略了一些接口细节),不过不同的部分是与理解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_reference<Widget&>::type产生的是Widget,所以std::forward边冲这样:

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

在返回类型和cast中都会发生引用折叠,导致被调用的最终版本的std::forward

Widget& forward(Widget& param)
{    return static_cast<Widget&>(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>。在std::forward的实现中用Widget代替T,像这样:

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>      // C++14,在命名空间std中
T&& forward(remove_reference_t<T>& param)
{
    return static_cast<T&&>(param);
}


引用折叠出现在四种上下文。第一种是最常见的,就是模板实例化。第二种是auto变量的类型生成。它的细节本质上和模板实例化相同,因为auto变量的类型推断和模板类型推断本质上相同(看条款2)。再次看回之前的一个例子:


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&。在声明中用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<typename T>
class Widget {
public:
    typedef T&& RvalueRefToT;
    ...
};

然后假如我们用一个左值引用来实例化Widget:

Widget<int&> w;

在Widget中用int&代替T,typedef变成这样:

typedef int& && RvalueRefToT;

引用折叠把代码弄出这样:

type int& RvalueRefToT;

这很明显的告诉我们,我们typedef选择的名字跟我们期望得到的不一样:当用左值引用实例化Widget时,RvalueRefToT是个左值引用的typedef

最后的一种会发生引用折叠的上下文是使用decltype中。如果,在分析一个使用decltype的类型期间,出现了对引用的引用,引用折叠会出面消除它。(关于decltype的详细信息,请看条款3。)

总结

需要记住的3点:

  • 引用折叠会出现在4中上下文:模板实例化,auto类型生成,typedef和类型别名声明的创建和使用,decltype
  • 当编译器在一个引用折叠上下文中生成了对引用的引用时,结果会变成一个引用。如果原来的引用中有一个是左值引用,结果就是个左值引用。否则,结果是个右值引用。
  • 通用引用是——出现在类型推断区分左值和右值和出现引用折叠的上下文中的——右值引用。
时间: 2024-10-20 03:06:58

Effective Modern C++ 条款28 理解引用折叠的相关文章

Effective C++:条款28:避免返回 handles 指向对象内部成员

(一) 有时候为了让一个对象尽量小,可以把数据放在另外一个辅助的struct中,然后再让一个类去指向它.看下面的代码: class Point { public: Point(int x, int y); void setX(int newVal); void setY(int newVal); }; struct RectData { Point ulhc; Point lrhc; }; class Rectangle { public: Point& upperLeft() const {

Effective Modern C++:05右值引用、移动语义和完美转发

移动语义使得编译器得以使用成本较低的移动操作,来代替成本较高的复制操作:完美转发使得人们可以撰写接收任意实参的函数模板,并将其转发到目标函数,目标函数会接收到与转发函数所接收到的完全相同的实参.右值引用是将这两个不相关的语言特性连接起来的底层语言机制,正是它使得移动语义和完美转发成了可能. 23:理解std::move和std::forward std::move并不进行任何移动,std::forward也不进行任何转发.这两者在运行期都无所作为,它们不会生成任何可执行代码.实际上,std::m

《Effective Modern C++》要点中英文对照

目录 CHAPTER 1 Deducing Types 章节1 类型推导 Item 1:Understand template type deduction. 条款1:理解模板类型推导. Item 2:Understand auto type deduction. 条款2:理解auto类型推导. Item 3:Understand decltype. 条款3:理解decltype. Item 4:Know how to view deduced types. 条款4:知道如何查看推导出来的类型.

Effective Modern C++ 读书笔记 Item 1

最近发现了<Effective Modern C++>这本书,作者正是大名鼎鼎的Scott Meyers——<Effective C++>.<Effective STL>的作者. 而就在C++11逐渐普及,甚至是C++14的新特性也进入大家的视野的时候,<Effective Modern C++>一书应运而生.此书与其前辈一样,通过数十个条款来展开,只不过这次是集中于C++11和C++14的新特性.auto.decltype.move.lambda表达式……

《Effective Modern C++》Item 1总结

Item 1: Understand template type deduction. 理解模板类型推导 template<typename T> void f(ParamType param); The type deduced for T is dependent not just on the type of expr, but also on the form of ParamType. 对于T类型推导,不仅依赖传入模板表达式也依赖ParamType的形式. ParamType is

[C++11] Effective Modern C++ 读书笔记

本文记录了我读Effective Modern C++时自己的一些理解和心得. item1:模板类型推导 1)reference属性不能通过传值参数传入模板函数.这就意味着如果模板函数需要一个reference类型的参数,必须在模板声明中将其声明为reference,否则,即使使用一个reference类型的变量调用模板函数,类型推导的结果将不带reference属性. 2)constant和volatile属性也不能通过传值参数传入模板函数,但是可以通过reference参数传入这些属性. 3

Effective C++:条款35:考虑virtual函数以外的其他选择

游戏中的人物伤害值计算问题. (一)方法(1):一般来讲可以使用虚函数的方法: class GameCharacter { public: virtual int healthValue() const; //返回人物的体力值,派生类可以做出修改 ... }; 这确实是一个显而易见的设计选择.但因为这样的设计过于显而易见,可能不会对其它可选方法给予足够的关注.我们来考虑一些处理这个问题的其它方法. (二)方法(2):使用NVI方法,在基类中使用一个公有的普通函数调用私有的虚函数. class G

Effective C++:条款36:绝不重新定义继承而来的non-virtual函数

(一)首先有下面的继承体系: class B { public: void mf(); ... }; class D : public B {...}; D x; 以下行为: B* pB = &x; pB->mf(); 异于以下行为: D* pD = &x; pD->mf(); 上面两种行为产生的结果不一定相同.看下面这种情况: mf是个non-virtual函数而D定义有自己的mf版本: class D : public B { public: void mf(); ...

Effective C++:条款37:绝不重新定义继承而来的缺省参数值

由于重新定义继承而来的non-virtual函数是不正确的(见上一个条款),所以这个条款就将问题局限于:绝不重新定义继承一个带有缺省参数值的virtual函数. (一) virtual函数是动态绑定的,而缺省参数却是静态绑定. 对象的所谓静态类型,是它在程序中被声明时所采用的类型. 你可能会在"调用一个定义于derived class 内的virtual函数"的同时,却使用了base class为它所指定的缺省参数值. (二) 为什么继承而来的virtual函数的缺省参数值不能被重新定