28.了解引用塌缩

前言

 
众所周知,当一个左值或右值初始化universal reference时将触发类型推衍,例如:

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

T将会根据传递至param的类型完成推衍、编码。

编码机制十分简单:当传入左值时,T被推导为左值引用。当传入右值时,T被推断为non-reference。因此:

1
2
3
4
Widget widgetFactory(); // function returning rvalue
Widget w; // a variable (an lvalue)
func(w); // call func with lvalue; T deduced to be Widget&
func(widgetFactory()); // call func with rvalue; T deduced to be Widget

显然,传入对象性质决定了universal reference的性质,这也正是forward的工作原理。


引用塌缩

 
在更加深入地了解std::forward与universal refernce之前,我们首先复习一个基础知识:C++禁止出现引用的引用(原因无非是引用并非对象),因此编译器会对如下行为作出警告:

1
2
3
int x;

auto& & rx = x; // error! can't declare reference to reference

细想一下,当一个左值引用被传递给形参为universal reference的函数模板时:
1
2
3
template<typename T>
void func(T&& param);
func(w); // invoke func with lvalue;T deduced as Widget&

如果我们推衍出T为Widget&,并以此实例化函数模板,则该模板可表示为:
1
void func(Widget& && param);

这里存在一个引用的引用,这时候问题来了,编译器是如何将函数签名更改为:
1
void func(Widget& param);

答案正是引用塌缩(reference collapsing)。尽管开发者们被禁止声明指向引用的引用,但编译器在特定环境下(例如模板实例化)可以默许这种行为的存在,并通过引用塌缩将其转为正确类型。


引用塌缩法则

由于引用共存在两种(左值、右值),因此引用的引用共存在四种情况:l-l,l-r,r-l,r-r,引用塌缩将按照如下法则将引用的引用转换为单一引用:只要存在左值引用,则最终结果为左值引用,因此仅有r-r为右值引用


引用塌缩与forward

 
引用塌缩是std::forward工作的核心所在,考虑下述实例:

1
2
3
4
5
template<typename T>
void f(T&& fParam){
// do some work
someFunc(std::forward<T>(fParam)); // forward fParam to someFunc
}

std::forward的工作是当且仅当T被推衍为non-reference时,将fParam(一个左值)强制转换为右值,其实现可表示为(与标准库相比缺少了一些接口描述):
1
2
3
4
template<typename T>
T&& forward(typename remove_reference<T>::type& param){
return static_cast<T&&>(param);
}

其中,typename remove_reference<T>::type& param保证了param必然以一个左值的身份接受强制转换。当T是一个左值(即类型推衍中T由一个左值初始化)时,T&&仍然是一个左值,因此param被转换为左值引用,返回一个左值。当T是一个non-reference(即类型推衍中T由一个右值初始化时),T&&为右值引用(r-r情况),此时左值引用param被转换为右值引用,而经函数返回的右值引用为右值,最终forward返回了右值。

在C++14中,forward可被更进一步简化为:

1
2
3
4
template<typename T>
T&& forward(remove_reference_t<T>& param){
return static_cast<T&&>(param);
}


引用塌缩的四大应用场景

auto对象生成

auto对象生成也将触发引用塌缩,其具体细节类似于模板实例化,假设当前存在声明与定义如下:

1
2
Widget widgetFactory(); // function returning rvalue
Widget w; // a variable (an lvalue)

当定义w1为:
1
auto&& w1 = w;

此时等价于:
1
Widget& && w1 = w;

触发引用塌缩转为:
1
Widget& w1 = w;

最终,w1为左值引用。

当定义w2为:

1
auto&& w2 = widgetFactory();

由于T被推衍为non-reference,此时并未触发引用塌缩,w2为Widget&&。

时至今日我们终于能够完全理解universal reference,它并非一种新的引用,而是仅在符合2种条件的特定环境下的右值引用:

  1. 区分左右值的类型推衍
    在此条件下,左值将推衍T为T&,右值则推衍T为non-reference。
  2. 触发引用塌缩

typedef与alias declaration

如果在typedef与alias declaration创建中出现引用的引用,则将触发引用塌缩,例如:

1
2
3
4
5
6
template<typename T>
class Widget {
public:
typedef T&& RvalueRefToT;

};

如果我们以一个左值引用实例化Widget:
1
Widget<int&> w;

此时RvalueRefToT将被表示为:
1
typedef int& && RvalueRefToT;

最终触发引用塌缩,RvalueRefToT被表示为:
1
typedef int& RvalueRefToT;

这一结果表明Typedef的结果似乎可能并非我们所愿。

decltype

在decltype推衍中如果存在引用的引用,则将利用引用塌缩予以消除。


总结

  1. 引用塌缩触发于四种特定环境:模板实例化、auto类型生成、typedef与alias declaration以及decltype推衍。
  2. 在引用塌缩过程中,只要存在左值引用,则最终结果为左值引用,否则即为右值引用。
  3. universal reference可视为特定环境下的右值引用。