条款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 = int → T&& = 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()
|
万能引用的定义
- 万能引用是满足两个条件的右值引用:
- 通过模板类型推导区分左值和右值,左值被推导为
T&
,右值被推导为 T
。 - 发生引用折叠,得到最终参数类型。
- 万能引用是一个特殊上下文中的右值引用,如模板函数参数或
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))
返回什么类型?
x
是 int
类型变量,(x)
是 对变量 x 的括号表达式,表达式是一个左值。- 根据 C++ 规则:
decltype((x))
—— 当表达式是一个 左值,decltype
返回 该表达式的引用类型,也就是 int&
。
所以:
替换 decltype((x))
带入后原表达式变成:
decltype((x))
是 int&
- 后面又接了两个
&
,所以类型写成了三层引用:外层引用 & ,里面还有 int& &
逐步折叠
第一次折叠,先折叠最内层两个引用:
int& &
,外层 &
,内层 &
,折叠结果是 &
,即:
折叠后剩下:
第二次折叠:
int& &
,同样外层 &
,内层 &
,折叠结果还是 &
:
decltype((x))& & rx = x;
最终等价于:
总结
- 引用折叠发生在这四种情况:
- 模板实例化
- auto 类型推导
- typedef/using 别名定义
- decltype 语境
- 规则:只要有左值引用,结果就是左值引用;否则是右值引用。
- 理解引用折叠是理解万能引用、完美转发和
std::forward
的关键。