指针的指针 ok, 引用的引用 no ---- 理解引用折叠

       我们都知道,在C/C++中,出现指针的指针,也就是二级指针的场景是合法的,甚至可以是更多级的指针,都是ok的;但是如果出现了引用的引用,那绝对是非法操作,任何一款C/C++的编译器都很乐意为您检测出此类非法操作。既然会讨论这个问题,说明 引用的引用 这样的场景是肯定会出现的,尤其是C++11标准以后;那该怎么办呢? 所以就顺势而为的出现了 引用折叠 的技术,顾名思义,这一技术就是将双重引用折叠成单个引用

int x;
auto& &rx = x;    //error. 不可以声明引用的引用

以上代码就是出现引用的引用的场景,胆敢写出这样的代码,编译器肯定会报错有问题的。

在之前文章探讨了有关模板函数类型推导以及auto类型推导之后,对下面代码中的推导结果便不再心存疑问了;

//默认下面的 Widget 为一个类

Widget createWidgetObj();    //返回Widget类型的右值;

Widget w;    //定义一个Widget类型的对象,左值;


template
void func(T&& param);


func(w);    // 实参为左值  T 的推导结果为 Wdiget&

func(createWidgetObj());    // 实参为右值  T 的推导结果为非引用类型的 Widget

在以上代码中,func(w) 的调用推导出T为 Widget& 类型;那如果就以此类型实例化该模板函数func会得到下面的结果:

void func(Widget& &¶m);

这不就是引用的引用吗?为什么编译器没有报错而且还得出了正确的推导结果(有点只许州官放火,不许百姓点灯的意思哦)!答案就是此处运用了引用折叠技术;凡是出现引用的引用的场景中,都会使用该技术,才得以保障它们顺利的运行下去;包括 std::forward() 的实现,其背后的关键也是引用折叠技术(后面会详细介绍)。

自C11之后出现了右值引用,加上之前的左值引用,所以就有了四种引用--引用的组合方式(左值引用--右值引用,左值引用--左值引用,右值引用--左值引用,右值引用--右值引用);如果以上这些引用的引用出现在了特殊的允许的语境中(如模板实例化,auto变量类型推导),该双重引用就会折叠成单个引用,具体规则如下:

如果双重引用中有任一引用为左值引用,则最终结果为左值引用;

否则(即双重引用均为右值引用时),最终结果为右值引用;

std::forward 的实现细节

我们都知道针对万能引用需要实施 std::forward,才能够保证原本实参为左值或右值属性不变的特性,尤其是需要将该参数转发给其他函数进行调用时,这点尤为重要。看下面一段代码:

template
void func(T&& fparam)
{
    someFunc(std::forward(fparam));    // 将形参 fparam 转发给 someFunc 函数使用
}

关于将参数转发给其他函数使用时,如何保证参数的左值右值属性信息不变的这一点,std::forward 是如何做到的呢?下面详细展开介绍。先给出一段简化的 std::forward 的实现源代码:

template
T&& forward(typename remove_reference::type& param)
{
    return static_cast(param);
}

实参为左值时

假设调用模板函数 func 时,实参为左值 Widget,则 T 被推导为 Widget& 类型;则对std::forward的实例化调用就变成了 std::forward(param),将Widget&代入forward的源码中会得到如下代码:

Widget& && forward(typename remove_reference::type& param)
{
    return static_case(param);
}

类型特征 typename remove_reference::type 产生的结果就是 Widget 类型,进而会有如下代码:

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

引用折叠技术在以上代码中应用了两处地方,一处是函数返回值类型,一处是强制类型转换;经过引用折叠之后得到最终的 forward 代码,如下:

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

正如所料,std::forward 接受一个左值引用,最终返回一个左值引用,内部的强制类型转换并未做任何事情,因为形参类型本身就已经是左值引用了;根据定义,返回左值引用即就是一个左值;所以从最开始调用模板函数 func 时,传入一个左值实参时,经 std::forward 返回一个左值 转发给 someFunc 函数,保持了实参左值属性的不变

实参为右值时

当以 Widget 类型的右值实参传给模板函数 func 时,T 被推导为非引用类型的 Widget,于是 std::forward 的源码就有了如下的变化:

Widget&& forward(typename remove_reference::type& param)
{
    return static_cast(param);
}

针对非引用类型Widget实施 remove_reference 操作的结果就和原类型 Widget 一样,所以 std::forward 就有了如下的变化:

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

注意:以上代码并没有发生引用折叠。也就是说,当实参以 Widget 类型的右值 传入模板函数 func 时,模板函数的形参 fparam 的类型被推导为 Widget 类型的右值引用Widget&&,但 fparam 本身是个左值,再经过 std::forward 的处理(std::forward 接受了一个 Widget&& 类型的左值 fparam,返回了一个Widget&& 类型的右值),将其强制转换为右值转发给someFunc函数,保持了最开始实参为右值的不变性

以上就是 std::forward 完美转发语义的实现,完美的地方就在于保证了实参左、右值属性的不变性;并将其转发给其他函数使用。

在C++14中出现了 模板别名 template alias  

template
	using remove_reference_t = typename remove_reference::type;

所以 std::forward 的源码会变得更简洁一些:

template 
T&& forward(remove_reference_t& param)
{
    return static_cast(param);
}

引用折叠出现的几种语境

引用折叠会出现的语境有四种。模板实例化以及auto变量的类型推导,其实这两种在本质上是一模一样的。在如下代码中,auto也能够模仿之前出现的代码

auto&& w1 = w;    // w 为 Widget 类型的左值,所以 auto ==> Widget&

Widget& && w1 = w;    // 出现引用的引用,经过引用折叠

Widget& w1 = w;    // w1 为左值引用


auto&& w2 = createWidget();    // 以Widget类型的右值初始化 w2; auto ==> Widget

Widget&& w2 = createWidget();    // 此处并没有引用的引用,w2 的类型为右值引用

第三种语境就是出现在 typedef 的类型别名声明中;假如原本想要在一个模板类中内置一个右值引用别名声明,如下:

template
class Widget
{
public:
    typedef T&& RvalueRefT;
...
...
}

如果以任意类型左值引用来实例化该模板类,则原本想声明的右值引用类型就变成了左值引用了,有点名不符实了;假如以 int& 来实例化该模板类,则有如下代码:

Widget w;

typedef int& && RvalueRefT;    // 引用的引用,应用引用折叠技术

typedef int& RvalueRefT;  // RvalueRefT 实际变成了左值引用,并不是原本想要的右值引用

最后一种会发生引用折叠的场景就是 decltype 的运用中,如果在分析一个涉及 decltype 类型的过程中出现了引用的引用,则会应用引用折叠技术而处理掉。

你可能感兴趣的:(c++,引用折叠,forward_完美转发)