文章

条款28:理解引用折叠

多层引用合并,规则如 & + && → &,防止产生非法引用类型。

条款28:理解引用折叠

条款28:理解引用折叠

万能引用的模板推导原理

1
2
template<typename T>
void func(T&& param);
  • 当传入左值时,T 会被推导为 左值引用类型,如 Widget&
  • 当传入右值时,T 会被推导为 非引用类型,如 Widget

示例:

1
2
3
4
5
Widget w;                     // 左值
Widget widgetFactory();       // 返回右值

func(w);                     // T = Widget&,param 类型为 Widget& &&
func(widgetFactory());       // T = Widget, param 类型为 Widget&&

C++中禁止声明引用的引用

1
2
int x;
auto& & rx = x;   // 错误!不能声明引用的引用
  • 虽然代码禁止写引用的引用,但模板实例化时可能会“产生”引用的引用。
  • 例如 func(w) 的形参 param 类型是 Widget& &&,这是引用的引用。

引用折叠规则(Reference Collapsing)

当出现引用的引用时,编译器会折叠为单个引用,规则是:

外层引用内层引用折叠结果示例代码说明
&&&T = int&T& = int& & → 折叠为 int&
&&&&T = int&&T& = int&& & → 折叠为 int&
&&&&T = int&T&& = int& && → 折叠为 int&
&&&&&&T = intT&& = int && → 保留为 int&&
  • 只要有左值引用(&),结果就是左值引用(&);否则才是右值引用(&&)。

例子解析

1
2
3
4
5
6
7
8
// 当传入左值时:
T = Widget&

// 参数类型展开:
Widget& && param

// 引用折叠后:
Widget& param

所以传入左值时,形参变为左值引用。

std::forward 的实现机制

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

万能引用折叠保证:

  • 传入左值时,T = Widget&,返回 Widget&,不会转换为右值。
  • 传入右值时,T = Widget,返回 Widget&&,实现完美转发。

auto&& 变量的引用折叠

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Widget w;
auto&& w1 = w;  // w 是左值

// 推导过程:
// auto 推导为 Widget&,所以 w1 的类型是 Widget& &&
// 引用折叠规则:Widget& && 折叠为 Widget&
// => 最终:w1 是 Widget& 类型,绑定到左值 w

auto&& w2 = widgetFactory();  // widgetFactory() 是右值

// 推导过程:
// auto 推导为 Widget(注意不是引用)
// 所以 w2 的类型是 Widget&&(右值引用)
// 引用折叠不发生(没有引用的引用)
// => 最终:w2 是 Widget&& 类型,绑定到右值 widgetFactory()

万能引用的定义

  • 万能引用是满足两个条件的右值引用:
    1. 通过模板类型推导区分左值和右值,左值被推导为 T&,右值被推导为 T
    2. 发生引用折叠,得到最终参数类型。
  • 万能引用是一个特殊上下文中的右值引用,如模板函数参数或auto&&

引用折叠发生的四种情况总结

模板实例化

如万能引用函数 template<typename T> void func(T&& param),传入左值会推导成 T = U&,折叠 U& &&U&

auto 类型推导

1
2
Widget w;
auto&& w1 = w;  // auto 推导为 Widget&,折叠为 Widget&

typedef 和别名声明中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 一个模板类,内部声明了一个 typedef 类型 T&&
template<typename T>
class Widget {
public:
    typedef T&& RvalueRefToT;  // 这个类型可能是引用的引用
};

// 实例化模板时传入 T = int&,即 T 是左值引用类型
Widget<int&> w;

// 此时 typedef 展开为:
typedef int& && RvalueRefToT;

// 由于是引用的引用,触发引用折叠规则:
// - 左值引用 & 与 右值引用 && 折叠结果是左值引用 &

// 所以最终:RvalueRefToT 被折叠为 int&

decltype 表达式中

如果 decltype 表达式涉及引用的引用,也会触发折叠:

1
2
int x;
decltype((x))& & rx = x;
先看 decltype((x)) 返回什么类型?
  • xint 类型变量,(x)对变量 x 的括号表达式,表达式是一个左值。
  • 根据 C++ 规则:decltype((x)) —— 当表达式是一个 左值decltype 返回 该表达式的引用类型,也就是 int&

所以:

1
decltype((x)) == int&
替换 decltype((x))

带入后原表达式变成:

1
int& & & rx = x;
  • decltype((x))int&
  • 后面又接了两个 &,所以类型写成了三层引用:外层引用 & ,里面还有 int& &
逐步折叠

第一次折叠,先折叠最内层两个引用:

  • int& &,外层 &,内层 &,折叠结果是 &,即:
1
int&

折叠后剩下:

1
int& & rx = x;

第二次折叠:

  • int& &,同样外层 &,内层 &,折叠结果还是 &
1
int&

decltype((x))& & rx = x; 最终等价于:

1
int& rx = x;

总结

  • 引用折叠发生在这四种情况:
    1. 模板实例化
    2. auto 类型推导
    3. typedef/using 别名定义
    4. decltype 语境
  • 规则:只要有左值引用,结果就是左值引用;否则是右值引用。
  • 理解引用折叠是理解万能引用完美转发std::forward的关键。
本文由作者按照 CC BY 4.0 进行授权