引用折叠是 C++11 引入的底层编译期规则 ,是通用引用(转发引用)、std::forward能正常工作的核心 ------C++ 语法明确禁止显式声明 "引用的引用" (比如int& &),但编译器在特定场景下会隐式生成引用的引用,此时靠 "引用折叠规则" 将其化简为单个引用,这也是通用引用能同时绑定左值 / 右值的根本原因。
通用引用的类型推导编码规则
对于通用引用模板:
c++
template<typename T>
void func(T&& param); // param是通用引用
模板参数T会根据传入实参的 "值类别"(左值 / 右值)做编码推导:
- 传入左值 →
T推导为左值引用(比如传入Widget左值,T=Widget&); - 传入右值 →
T推导为非引用类型(比如传入Widget右值,T=Widget)。
这种 "不对称编码" 是理解引用折叠的关键,比如:
c++
Widget w; // 左值
Widget widgetFactory(); // 返回右值的函数
func(w); // 左值传参 → T推导为Widget&
func(widgetFactory()); // 右值传参 → T推导为Widget
引用折叠的规则
1. 为什么需要引用折叠?
以上面左值传参为例:把推导后的T=Widget&代入模板,会得到:
c++
void func(Widget& && param); // 引用的引用(显式写会报错,但编译器隐式生成时允许)
编译器不会报错,而是通过引用折叠规则将 "引用的引用" 化简为单个引用。
2. 折叠规则(有左则左,全右才右)
C++ 有左值引用(T&)和右值引用(T&&)两种,因此 "引用的引用" 有 4 种组合,折叠后仅两种结果:
| 引用组合 | 折叠结果 | 规则说明 |
|---|---|---|
| 左值引用 + 左值引用 | 左值引用 | 只要有一个是左值引用,结果就是左值引用 |
| 左值引用 + 右值引用 | 左值引用 | 同上 |
| 右值引用 + 左值引用 | 左值引用 | 同上 |
| 右值引用 + 右值引用 | 右值引用 | 只有全是右值引用,结果才是右值引用 |
因此,Widget& && 折叠后就是 Widget&,最终函数签名为:
c++
void func(Widget& param); // 通用引用绑定左值,最终是左值引用
引用折叠是 std::forward 的底层实现原理
std::forward的 "完美转发" 能力,完全依赖引用折叠规则。
1. std::forward 的简化实现(C++11 版)
c++
template<typename T>
T&& forward(typename remove_reference<T>::type& param) {
return static_cast<T&&>(param);
}
(C++14 可通过remove_reference_t<T>简化,去掉typename和::type)
2. 分场景拆解 forward 的实例化(结合引用折叠)
场景 1:传入左值(比如Widget w; func(w);)
T推导为Widget&,代入 forward:
c++
// 第一步:代入T=Widget&
Widget& && forward(remove_reference<Widget&>::type& param) {
return static_cast<Widget& &&>(param);
}
// 第二步:remove_reference<Widget&>::type = Widget
Widget& && forward(Widget& param) {
return static_cast<Widget& &&>(param);
}
// 第三步:引用折叠化简
Widget& forward(Widget& param) {
return static_cast<Widget&>(param);
}
```
- 结果:forward 返回左值引用(左值),无任何转换,左值实参 "原样转发"。
#### 场景 2:传入右值(比如`func(widgetFactory());`)
- `T`推导为`Widget`,代入 forward:
```c++
// 第一步:代入T=Widget
Widget&& forward(remove_reference<Widget>::type& param) {
return static_cast<Widget&&>(param);
}
// 第二步:remove_reference<Widget>::type = Widget
Widget&& forward(Widget& param) {
return static_cast<Widget&&>(param);
}
```
- 结果:forward 返回右值引用(右值),把原本是左值的形参`param`转为右值,实现 "右值转发"。
`std::forward`的本质是:**利用引用折叠规则,根据 T 的推导类型(左值引用 / 非引用),有条件地将形参转为右值**------ 只有传入的是右值时,才转成右值;传入左值时,保持左值。
## 引用折叠发生的 4 种场景
编译器只会在 4 种特定场景下隐式生成 "引用的引用",并触发引用折叠,条款 28 逐一详解:
### 场景 1:模板实例化(最常见)
通用引用模板实例化时的类型替换(如前面`func`的例子),是引用折叠最核心的应用场景。
### 场景 2:auto 类型推导
`auto&&`的推导逻辑和模板完全一致(通用引用的另一种形式),推导后触发折叠:
```c++
Widget w; // 左值
Widget widgetFactory(); // 右值
// 场景2.1:auto&&绑定左值
auto&& w1 = w;
// auto推导为Widget& → 代入得Widget& && → 折叠为Widget&(左值引用)
// 场景2.2:auto&&绑定右值
auto&& w2 = widgetFactory();
// auto推导为Widget → 代入得Widget&& → 无折叠(右值引用)
场景 3:typedef / 别名声明的创建与使用
创建 typedef / 别名时若出现 "引用的引用",触发折叠:
c++
template<typename T>
class Widget {
public:
typedef T&& RvalueRefToT; // 声明"右值引用"的typedef
};
// 用左值引用实例化Widget
Widget<int&> w;
// 代入T=int& → typedef int& && RvalueRefToT → 折叠为int&
// 最终typedef名"RvalueRefToT"实际是左值引用(名字和实际类型不符)
场景 4:decltype 的使用
分析decltype表达式时若出现 "引用的引用",触发折叠(参考 Item3):
比如通过复杂表达式推导后得到 "引用的引用",编译器会按规则折叠为单个引用。
通用引用的本质
通用引用不是新类型,它只是 "满足两个条件的右值引用(T&&)":
- 类型推导能区分左值 / 右值(T 的推导编码规则:左值→T&,右值→T);
- 推导后的类型代入会触发引用折叠。
简单说:T&&只有在 "能推导 + 会折叠" 的上下文里,才是通用引用;否则就是普通右值引用。
总结
- 引用折叠仅发生在 4 种场景:模板实例化、auto 类型推导、typedef / 别名声明、decltype 使用;
- 折叠规则:有左值引用参与则结果为左值引用,仅全为右值引用时结果为右值引用;
- 通用引用是 "特定上下文的右值引用"------ 上下文需满足 "类型推导区分左 / 右值 + 触发引用折叠";
- 引用折叠是通用引用、std::forward 能工作的底层核心。