28 Understand reference collapsing
对于模板函数
而言,不管param 是左值还是右值,推导模板参数 T 都会编码(encode)。
编码的机制很简单。当左值被传入的时候,T 被推导为左值引用,当右值被传入的时候,T 推导为非引用。注意,这里并不是对称的。因此
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
Widget 传入 func,依赖于其是左值还是右值,推导的类型不同。我们很快将看到,这决定了通用引用是左值引用还是右值引用,这也是 std::forward 正确工作的机制。
在继续之前,我们首先要明确在 C++ 中引用的引用是不合法的。
不过再看看当左值传入模板函数的时候发生了什么:template <typename T>
void func(T &¶m); // as before
func(w); // invoke func with lvalue; T deduced as Widget&
T 被推导成引用 Widget&,那么函数参数看起来如下
引用的引用。编译器自己生成了引用的引用,但是没有报错。Item 24 说过,当 param 用左值初始化,那么其类型是左值引用,编译器是如何使得最后的签名如下的呢?
答案是引用折叠(reference collapsing)。当编译器遇到引用的引用时(比如这里的模板实例化),引用折叠指示下一步需要做什么。
有左值和右值两种引用,那么引用的引用有四种,左值的左值,左值的右值,右值的左值,右值的右值。如果出现了引用的引用,根据如下规则折叠成单一引用。
如果任意一层引用是左值引用,结果是左值引用;否则,即两层都是右值引用,结果是右值引用。
上面将 Widget& 替换 func 的例子中,产生了左值引用的右值引用,那么结果就是左值引用。
引用折叠是 std::forward 能够工作的关键之一。std::forward 的一个常见场景是有通用引用的模板函数。
template <typename T>
void f(T &&fParam)
{
// do some work
someFunc(std::forward<T>(fParam)); // forward fParam to someFunc
}
fParam 是通用引用,那么不管传入 f 的是左值还是右值,T 都会编码。当 T 编码说传入 f 的是右值时,std::forward 的就是要将 fParam 这个左值转化成右值。
std::forward 实现如下
template <typename T> // in
T &&forward(typename // namespace
remove_reference<T>::type ¶m) // std
{
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 ¶m)
{
return static_cast<Widget & &&>(param);
}
std::remove_reference<Widget&>::type 结果是 Widget,因此代码变成了
对类型转换和返回类型应用引用折叠,最终版本的 std::forward 如下。
当左值传入 f,std::forward 被实例化成接受左值引用并返回左值引用,std::forward 内部什么也没有做,因为传入参数已经是 Widget& 类型了。左值传入 std::forward,返回了左值引用,而后者依旧是左值,因此 std::forward 返回的还是左值,符合预期。
现在假定传入 f 的是 Widget 的右值。这种情况下,T 被推导为 Widget。f 内部 std::forward 被实例化为 std::forward<Widget>,使用 Widget 替换 T 得到 std::forward 的实现。
Widget &&forward(typename remove_reference<Widget>::type ¶m)
{
return static_cast<Widget &&>(param);
}
std::remove_reference 作用于 Widget 类型,结果还是 Widget 类型,所以 std::forward 代码是
这里没有引用的应用,也就没有引用折叠,即这就是 std::forward 的最终版本。
从函数返回的右值引用被定义为右值。std::forward 将传入 f 的 fParam 这个左值转换为了右值。从最终效果来看,传入 f 的右值被右值的形式转发给了 someFunc,这就是我们所期望的。
C++14 中的 std::remove_reference_t 能够简化 std::forward 的实现。
template <typename T> // C++14
T &&forward(remove_reference_t<T> ¶m)
{
return static_cast<T &&>(param);
}
auto 声明变量。细节与模板基本一致,因为 Item 2 告诉我们 auto 的类型推导与模板基本一致。考虑之前的例子
template <typename T>
void func(T &¶m);
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
auto 上下文中,规则是类似的。
使用左值 w 初始化,auto 推导的类型是 Widget&,将其代回 auto 声明得到了引用的引用。
引用折叠
所以 w1 是左值引用。
另一方面,我们看下面这个声明
使用右值初始化w2,auto 的推导结果是没有引用的 Widget,代回 auto 声明
这里没有引用的引用,就是最后结果了。因此 w2 是右值引用。
至此,基本完全理解了通用引用。它不是一种新的引用类型,当下面两个条件满足的时候是右值引用。
- 类型推导区分左值和右值。
T类型是左值,那么推导成T&,如果是右值,还是T本身。 - 发生引用折叠。
通用引用的概念很有用。它使得我们可以不必知道引用折叠的存在,不必推导左值和右值不同的类型,然后再使用引用折叠的规则。
第三个出现引用折叠的地方是使用 typedef 或声明别名,如果在这个过程出现了引用的引用,就会发生引用折叠。比如我们有 Widget 模板类,内部使用 typedef 定义一个右值引用类型。
Widget 的是左值引用
T 是 int&,代入 typedef 语句得到
引用折叠完结果是
typedef 的名字可能并不能描述实际情况,当使用左值引用实例化 Widget 时,RvalueRefToT 是左值引用。
最后一处是使用 decltype 声明类型时,如果出现了引用的引用,就会发生引用折叠。
Things to Remember
- Reference collapsing occurs in four contexts: template instantiation,
autotype generation, creation and use oftypedefand alias declarations, anddecltype. - When compilers generate a reference to a reference in a reference collapsing context, the result becomes a single reference. If either of the original references is an lvalue reference, the result is an lvalue reference. Otherwise it's an rvalue reference.
- Universal references are rvalue references in contexts where type deduction distinguishes lvalues from rvalues and where reference collapsing occurs.