我们都知道,在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,才能够保证原本实参为左值或右值属性不变的特性,尤其是需要将该参数转发给其他函数进行调用时,这点尤为重要。看下面一段代码:
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
Widget& && forward(typename remove_reference::type& param)
{
return static_case(param);
}
类型特征 typename remove_reference
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 类型的过程中出现了引用的引用,则会应用引用折叠技术而处理掉。