理解引用折叠

本文最后更新于:April 20, 2022 am

本篇为介绍右值引用、移动语义和完美转发系列博客的第六篇,讨论引用折叠

右值引用,移动语义、完美转发系列目录:右值、移动语义和完美转发

万能引用到底是什么?

Item23:理解std::move和std::forward 的讨论中,我们知道了实参被传入函数模板时,推导出来的模板类型参数能够体现传入的实参是左值还是右值。但没有明确的是,这一情况的必要条件是函数模板的形参是万能引用。随后,在 Item24:区分万能引用和右值引用 的讨论中,我们讨论了右值引用和万能引用。

上述的讨论表明了一点,即像这样的万能引用可以包含实参的左值或右值信息

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

实现的机制也比较简单,当左值作为实参时,T将被推导为左值引用。当右值作为实参是,T将会被推导为非引用类型。注意这是不对称的!

1
2
3
4
5
6
Widget widgetFactory();
// a factory function that returns rvalue

Widget w;
func(w); //T推导为Widget&
func(widgetFactory()); //T推导为Widget

在第五行和第六行的两次调用中,实参类型都是Widget,但是第一次传入了左值,第二次传入了右值,随即函数模板的类型参数T被推导为相应的类型[1]

下文将会详细讨论背后的机制,它被叫做引用折叠,也正是它决定了万能引用到底是右值引用还是左值引用,并且这也是std::forward赖以工作的核心。

引用折叠

引用的引用

在我们细看之前,需要先了解一个事实——在C++中,不允许创建引用的引用。

1
2
3
4
5
int x;
...
auto && rx=x; // 正确,rx为万能引用!
auto & &rx1=x; // 错误,试图创建引用的引用!
int & &rx2=x; // 错误,试图创建引用的引用!

如果试图这么做,编译器会抛出错误,提示不能创建指向auto&或者int&的引用

回看上面的例子

1
2
3
4
5
6
template <typename T>
void fun(T&& param){
...
}
Widget w;
func(w);

实参为左值时,万能引用的类型参数T将会被推导为左值引用,再加上后面的两个引用符号,就成了Widget& &&了,这也是引用的引用!从 Item24:区分万能引用和右值引用 的讨论中,我们知道因为万能引用的函数模板的实参是左值,因此最终param的类型是左值引用,也就是Widget&

如果看一下编译后的代码,会发现最终的函数签名是

void func(Widget& param);, 编译器是如何走到这一步的呢?

虽然开发者不能声明引用的引用,但是编译器却可以在四种语境下使用它。其中第一种,也是最常见的一种,就是模板的实例化。当编译器生成了引用的引用之后,引用折叠的机制会决定接下发生什么

引用折叠出现的四种场景

模板实例化

既然引用有左值引用和右值引用两种,那么引用的引用就可能有四种不同的组合。如果出现了引用的引用的情况,并且它在被允许的场合出现,引用折叠的机制就会发挥作用:

==如果两个引用中任何一个是左值引用,那么最终的结果是左值引用。否则结果是右值引用==

在上面的例子中,用Widget&代替类型参数T之后,得到参数类型Widget& &&,这是一个左值引用的右值引用,根据规则最终会折叠成左值引用Widget&

引用折叠是使得std::forward能够工作的关键,通常,我们会像这样使用完美转发:

1
2
3
4
template<typename T> void f(T&& fParam){
...
someFunction(std::forward<T>(param));
}

分析std::forward的作用:当且仅当类型参数T表明用于初始化形参fParam的实参表达式是右值时,将fParam(左值)转型成右值。

std::forward的行为可以这样来概括(不是标准实现,但是可以概括其行为)

1
2
3
4
template<typename T> T&& forwawrd(
typename remove_reference<T>::type& param){
return static_cast<T&&>(param);
}

假设传入函数f的实参是一个Widget类型的左值,那么在函数f中,类型参数T将会被推导为Widget&[2]。进而,对std::forward调用时,如果我们将类型参数T进行简单的替换,得到

1
2
3
Widget& && forward(typename remove_reference<Widget&>::type& param){
return static_cast<Widget& &&> (param);
}

我们知道类型萃取remove_reference<Widget&>::type将会得到Widget,再经过引用折叠,最终得到

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

正如我们所见,当函数f的实参是左值表达式时,std::forward<T>(param)将会拿到一个左值引用param,然后返回一个左值引用。

从类型转换上讲,此时std::forward什么也没干——毕竟参数是左值引用,返回的也是左值引用,而且两者都是同一类型的引用。而且,我们注意到,当函数返回一个左值引用时,函数的返回值也是左值。

现在我们假设函数f的实参被右值表达式初始化,因此类型参数T被推导为Widget。再一次将它代入std::forward,我们注意到此次没有引用折叠的情况出现,化简得到

1
2
3
4
Widget&& forward(
Widget& param){
return static_cast<Widget&&> (param);
}

函数返回的右值引用是右值,因此std::forward将会把f的形参fParam(一个左值)转换成一个右值。最终,当f的形参被右值表达式初始化时,开始的右值将会被转发给someFunc,正如我们所期待的那样。

auto的类型生成

引用折叠出现在四种语境下,最常见的一种就是模板实例化了。第二种是用auto声明的变量的类型推导。具体的细节和模板一模一样,因为于auto声明的变量的类型推导的过程和模板就是一样的(参见Item2)。

我们再看看上面的例子代码

1
2
3
4
5
6
7
template <typename T>void func(T&& param)){...}

Widget widgetFactory(){...}

Widget w;
func(w);
func(widgetFactory())

第六行用左值调用函数模板,类型推导过程就可以类比为

1
auto&& w1= w;

用左值初始化w1,因此auto被推导为Widget&,经过引用折叠,最终w1的类型是Widget&

第七行用右值调用函数模板,类型推导过程就可以类比为

1
auto&& w2= widgetFactory()

用右值初始化w2,因此auto被推导为Widget,不经过引用折叠,最终w2的类型是Widget&&

至此,我们真正地理解了Item24中提出的”假概念“:万能引用。万能引用并不是一种新的引用,它事实上只是一种右值引用,但是出现时还满足下面两个条件:

  • 类型推导区分左值和右值。左值使得类型参数T被推导为T&,右值使得类型参数T被推导为T
  • 发生了引用折叠

万能引用这一概念很有用,因为它可以屏蔽引用折叠的具体细节,使我们专注于引用折叠的结果

typedef和别名

第三种场景是typedef和别名声明的生成(参见Item9)。

如果在创建或者评估过程中出现了指向引用的引用的情况,引用折叠机制也会被触发。举例来说,我们在Widget类中额外定义一个类型别名

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

然后,进行使用左值引用的类型进行实例化

1
Widget<int&> w;

TWidget&代替之后,有typedef int& && RvalueRefToT;,此时需要引用折叠,将之简化为typedef int& RvalueRefToT;

可见,此处的类型别名起得并不那么适宜,因为用左值引用实例化Widget类就会导致类型被折叠成左值引用。可以考虑将T用类型萃取std::remove_reference_t<T>代替

decltype

最后一个场景,当decltype中的解析有指向引用的引用情景出现时,也会触发引用折叠。

总结

引用折叠只会出现在四种语境下:模板实例化,auto的类型生成,创建和使用typedef和别名声明,以及decltype

当编译器生成了指向引用的引用时,其结果会被折叠成单个引用。当两个引用之中至少有一个是左值引用时,结果是左值引用,否则是右值引用。

万能引用是右值引用的在类型推导能够区分左值和右值的语境下的特殊表现


  1. 这里是T被推导为Widget&或者Widget,而非T&&被推导为Widget&或者Widget
  2. 注意,std::forward只需要接收左值引用即可,因为函数的形参是万能引用。我们知道,函数的形参一定是左值,引用也一定是左值

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!