C++ 完美转发与引用折叠
更新时间:2026-08-27。本文是
languages/cpp/主题高手层第 3 篇。上一篇讲了std::move只做类型转换,这一篇聊它的兄弟std::forward——同样只做类型转换,但方向正好相反:move把一切转成右值,forward则尽量保持原样。围绕它展开的"引用折叠"和"万能引用",是读懂现代 C++ 泛型代码的钥匙。
本文要回答的问题
T&&在模板里到底是什么?为什么传左值它变左值引用、传右值变右值引用?- "引用折叠"四条规则是什么?
A& &这种"引用的引用"为什么能编译? std::forward的实现为什么只有一行static_cast?- 什么时候该用
forward,什么时候该用move?
引用的引用:C++ 编译器替你"折叠"
先抛一个问题:下面的代码能编译吗?
int x = 1;
int &ref = x;
int & &ref_ref = ref; // "引用的引用"?答案是:能编译。C++ 里不允许你直接声明"引用的引用",但当它经过模板参数推导产生时,编译器会执行引用折叠(reference collapsing),把它变成普通引用。上面这句等价于 int &ref_ref = ref;。
引用折叠的规则只有四条,规律非常好记——只要出现一个左值引用 &,结果就是左值引用;只有两个都是右值引用 &&,结果才是右值引用:
| 组合 | 折叠结果 | 记忆法 |
|---|---|---|
T& & | T& | 有左值就是左值 |
T& && | T& | 有左值就是左值 |
T&& & | T& | 有左值就是左值 |
T&& && | T&& | 全是右值才是右值 |
using LRef = int&;
using RRef = int&&;
using A = LRef&; // int& & → int&
using B = LRef&&; // int& && → int&
using C = RRef&; // int&& & → int&
using D = RRef&&; // int&& && → int&&一句话记忆:左值引用有"一票否决权"——四种组合里但凡有一个 &,结果都是 &。
万能引用:T&& 的两个面孔
&& 出现在两个地方,含义完全不同:
void f(int &&r); // 右值引用:只能绑右值
template <typename T>
void g(T &&arg); // 万能引用:既能绑左值又能绑右值T&& 只有当 T 是模板推导的类型参数(或 auto&&)时才是万能引用(也叫转发引用)。它的行为:
| 实参 | 推导出的 T | 参数类型 | 说明 |
|---|---|---|---|
左值 int | int& | int& && → int& | 万能引用变左值引用 |
右值 42 | int | int&& | 万能引用变右值引用 |
const 左值 | const int& | const int& | const 保留 |
int x = 1;
g(x); // T = int&,参数折叠为 int&
g(42); // T = int,参数为 int&&这里的关键:万能引用通过引用折叠"同时接收左值和右值",而且不丢失实参的 const 属性。这正是"完美转发"的地基——信息在传递过程中不失真。
下面这张图把 move 和 forward 的差别画清楚:

std::forward:一行代码的"聪明"
std::forward 的标准实现(简化版)就一句话:
template <typename T>
constexpr T&& forward(std::remove_reference_t<T>& t) noexcept {
return static_cast<T&&>(t); // 把左值 t 转成 T&&
}配合万能引用,它做的事是:
template <typename T>
void wrapper(T &&arg) {
inner(std::forward<T>(arg));
}推演一下两条路径:
- 传左值:
T = int&→std::forward<int&>(arg)的返回类型是int& &&→ 折叠为int&。转发成左值,inner收到左值引用。 - 传右值:
T = int→std::forward<int>(arg)返回int&&。转发成右值,inner收到右值引用,可以触发移动。
所以 forward 的名字很贴切:把实参原有的值类别(左值/右值)原封不动"转发"给下一层。参数在 wrapper 内部是左值(有名字),经过 forward<T> 恢复了它进函数前的身份。
forward vs move:别用混
两者底层都是 static_cast,区别只在"转成什么":
| 对比项 | std::move | std::forward |
|---|---|---|
| 本质 | 无条件转右值引用 | 按模板参数 T 恢复原值类别 |
| 适用场景 | 明确要转移资源 | 泛型代码里转发参数 |
| 典型用法 | v.push_back(std::move(obj)) | inner(std::forward<T>(arg)) |
| 对左值 | 变成右值(危险) | 保持左值 |
| 对右值 | 保持右值 | 保持右值 |
用错的反面教材:转发函数里用 move 代替 forward,会把左值实参也变成右值传下去——如果 inner 恰好有移动构造重载,左值对象就被悄悄"掏空"了。这是隐蔽 bug 的高发点。
template <typename T>
void bad_wrapper(T &&arg) {
inner(std::move(arg)); // 错!左值也被转成右值
}完美转发的完整例子
把前面几块拼起来,看一个真实场景——写一个"记录日志再执行"的通用包装器:
#include <utility>
#include <iostream>
void take(int &x) { std::cout << "lvalue: " << x << "\n"; }
void take(int &&x) { std::cout << "rvalue: " << x << "\n"; }
template <typename T>
void log_and_take(T &&arg) {
std::cout << "log...\n";
take(std::forward<T>(arg)); // 值类别原样转发
}
int main() {
int a = 10;
log_and_take(a); // 输出 lvalue: 10(a 仍是左值)
log_and_take(20); // 输出 rvalue: 20(临时对象保持右值)
}如果这里写 std::move,第一行也会打印 rvalue——值类别信息被破坏了。forward 的存在,就是为了让泛型包装器像"透明层"一样不改变参数属性。
与本站性能主线衔接
- 移动语义的延伸:
forward是泛型代码里的"移动开关",正确转发才能让inner的移动构造生效,见 移动语义与右值引用。 - STL 源码:
make_unique、emplace_back内部都靠完美转发把构造参数传给目标构造函数,见 make_unique / make_shared。 - 零成本抽象:引用折叠与转发都是编译期机制,运行时零开销——与 L4 编译器选项 中模板展开的思路同源。
- 陷阱排查:转发写错导致的无谓拷贝,会在 perf 火焰图中表现为热点里的
memcpy。
一句话总结
完美转发 = 万能引用(T&&)+ 引用折叠(有 & 就是 &)+ std::forward(按 T 恢复值类别)三层配合,让泛型包装器把参数的左值/右值身份原封不动传给下一层;move 是无条件右值化,forward 是条件保持,转发场景用后者。