C++ 完美转发
C++ 完美转发
完美转发 是指:函数模板接收到的参数在传递给其他函数时,保持其原本的“值类别”(即左值或右值)。
- 常用于 泛型函数模板 中,用于“转发参数给其他函数”时,避免复制或不必要的拷贝构造。
- C++11 引入
std::forward和&&引用折叠来实现完美转发。
完美转发的关键
- 使用 万能引用(Forwarding Reference):
template<typename T>
void func(T&& arg); // T&& 是万能引用
-
搭配
std::forward<T>(arg)保留参数的值类别std::forward<T>(arg);根据 T 的类型判断是否将
arg保持为右值:- 如果
T是int&,则结果为左值 - 如果
T是int&&,则结果为右值
即它使得模板参数保留了传入的原始“引用性质”。
- 如果
右值引用对比万能引用
右值引用(Rvalue Reference)
- 当类型是明确指定时,
int&&就是右值引用。 - 只能绑定到右值(临时对象或
std::move后的对象)。
int&& r = 5; // 合法,5 是右值
int&& r2 = x; // 错误,x 是左值
万能引用(Universal / Forwarding Reference)
-
只有在模板类型推导时,形如
T&&的参数被称为万能引用(Scott Meyers 在《Effective Modern C++》中提出的名词)。 -
其行为如下:
| 传入参数的值类别 | 推导出的 T 类型 |
形参类型 T&& 实际类型 |
|---|---|---|
| 传入左值 | T 被推导为 int& |
变成 int& &&,折叠为 int&(左值引用) |
| 传入右值 | T 被推导为 int |
仍是 int&&(右值引用) |
简言之:万能引用会根据实参的值类别推导为左值引用或右值引用,达到“完美转发”的效果。
引用折叠规则(Reference Collapsing)
| 形式 | 折叠结果 |
|---|---|
T& & |
T& |
T& && |
T& |
T&& & |
T& |
T&& && |
T&& |
所以当 T 推导为左值引用时,T&& 实际变成左值引用。
示例对比:普通转发 vs 完美转发
#include <iostream>
#include <utility>
// 有两个版本的 print,分别接受 左值引用 和 右值引用,用来区分传入的参数到底是左值还是右值
void print(int& x) { std::cout << "左值引用\n"; }
void print(int&& x) { std::cout << "右值引用\n"; }
// 普通传递(失去值类别信息)
template<typename T>
void pass(T arg) {
// 参数 arg 是传值方式,即函数内部的 arg 是局部变量,无论传入左值还是右值,函数体内的 arg 都是左值(因为它有名字且可寻址)
// 传递给 print(arg) 时,arg 是左值,所以总是调用 print(int&)
print(arg);
}
// 完美转发
template<typename T>
// T&& 是万能引用,arg 可以绑定左值或右值
void perfectForward(T&& arg) {
// 关键是 std::forward<T>(arg),它能根据 T 的类型推导完美地转发参数的值类别
// 如果传入左值,T 推导为 int&,std::forward<int&>(arg) 返回 int&,调用 print(int&)
// 如果传入右值,T 推导为 int,std::forward<int>(arg) 返回 int&&,调用 print(int&&)
print(std::forward<T>(arg));
}
int main() {
int x = 10;
pass(x); // 传入左值 x
// arg 是传值,传入左值,arg 为左值
// 调用 print(int&),输出 "左值引用"
pass(20); // 传入右值 20
// arg 是传值,虽然传入右值,arg 是局部变量左值
// 调用 print(int&),输出 "左值引用"
perfectForward(x); // 传入左值 x
// T 推导为 int&,std::forward 返回左值引用
// 调用 print(int&),输出 "左值引用"
perfectForward(20); // 传入右值 20
// T 推导为 int,std::forward 返回右值引用
// 调用 print(int&&),输出 "右值引用"
return 0;
}
输出:
左值引用
左值引用
左值引用
右值引用
重载解析和引用绑定规则
如果传的是一个左值,比如变量 x。调用 print(x),有三个版本:
void print(int& x); // 接收左值引用
void print(int&& x); // 接收右值引用
void print(int x); // 按值传递(拷贝)
- 左值只能绑定到左值引用(
int&)或者按值传递(int x),不能绑定到右值引用(int&&)。 - 所以编译器排除
print(int&&)。 - 这时编译器会在剩下的两个中选择最佳匹配:
int&:直接引用,无需拷贝int:按值拷贝,调用成本更高
所以选择 print(int& x)。
-
左值绑定规则:左值可绑定到左值引用或按值参数,不能绑定到右值引用。
-
右值绑定规则:右值可绑定到右值引用或按值参数,不能绑定到左值引用。
重载解析会选最佳匹配,避免不必要拷贝。
关键点
模板形参写成 T&&,但这不是普通的右值引用,它是万能引用(转发引用)
-
传入 左值 时,模板推导时 T 会变成左值引用类型,比如
int&。 -
传入 右值 时,模板推导时 T 会变成普通类型,比如
int。
为什么传左值,T 变成 int&?
-
形参写的是
T&&,如果T直接是int,那么形参是int&&(右值引用)。但这时传进来了左值x,右值引用是不能直接绑定左值的,编译器不能直接让int&&绑定左值。所以编译器让T推导为int&,于是形参类型是int& &&。 -
这里的
int& &&是引用的引用,看着奇怪,但 C++ 有引用折叠规则:& &&折叠成&,所以int& &&等同于int&。这样形参类型就变成了int&,成功绑定左值。 -
换句话说:
-
左值传给
T&&时,编译器会“升级”T为int&,保证T&&最终变成int&,能接受左值。 -
这样模板就能接受左值,参数仍保持左值语义。
-
为什么传右值,T 变成 int?
- 形参写的是
T&&,这是一种万能引用(也叫转发引用),既能接受左值,也能接受右值。当你传入一个右值,比如20,编译器会推导T = int,于是形参类型变为int&&,也就是标准的右值引用。 - 这没问题,因为右值可以直接绑定到右值引用,所以不需要任何折叠或者特殊处理。
- 换句话说:
- 右值传给
T&&时,编译器会推导出T = int,于是T&&就是int&&,刚好可以接受右值。 - 所以整个推导链条非常自然:右值 →
T = int→ 形参是int&&→ 成功绑定右值。
- 右值传给
总结图表
| 函数调用 | 传入参数类别 | 模板参数 T 类型 |
参数 arg 类型 |
std::forward<T>(arg) 类型 |
传给 print 的参数类型 |
输出 |
|---|---|---|---|---|---|---|
pass(x) |
左值 | int |
int(局部变量,左值) |
—(未用 forward) | 左值引用 | 左值引用 |
pass(20) |
右值 | int |
int(局部变量,左值) |
—(未用 forward) | 左值引用 | 左值引用 |
perfectForward(x) |
左值 | int& |
int&(折叠后) |
int& |
左值引用 | 左值引用 |
perfectForward(20) |
右值 | int |
int&& |
int&& |
右值引用 | 右值引用 |
- 普通传递时,传入的参数无论左值还是右值,都会被拷贝或移动到局部变量,变成左值。
- 完美转发借助万能引用和
std::forward,可以保持参数的原始值类别,避免值类别丢失。
完美转发的本质
完美转发的关键目标是:
尽可能将调用者的实参值类别(左值或右值)“原样”地传递给被调用者(callee)。
这个值类别信息必须通过两次保留和传递,缺一不可:
第一次传递:通过万能引用 T&& arg
template<typename T>
void func(T&& arg) { ... }
- 当传入左值时,
T推导为int&,则函数参数类型为int& &&,折叠为int&。 - 当传入右值时,
T推导为int,则函数参数类型为int&&。 - 也就是说:
T&& arg作为万能引用,可以保留原始值类别信息在类型T中。- 然而!
arg作为具名变量,在函数体中始终是左值!
第二次传递:使用 std::forward<T>(arg)
- 虽然
arg是左值,但我们可以利用T这个保存了“原始值类别”的类型信息,来“恢复”传入的值类别。 - 这就是
std::forward<T>(arg)做的事情:- 如果
T是int&,则std::forward<T>(arg)是左值引用(传左值)。 - 如果
T是int,则std::forward<T>(arg)是右值引用(传右值)。
- 如果
std::forward就像是“值类别的还原器”。
对比总结
传入实参 --> T&& arg (保存值类别) --> arg 是左值 --> std::forward<T>(arg) 恢复值类别 --> 最终传入目标函数
| 传入实参 | T 推导 | arg 的类型 | arg 本身 | std::forward(arg) 的效果 | 传入 print 的版本 |
|---|---|---|---|---|---|
| x (左值) | int& | int& | 左值 | 左值 | print(int&) |
| 20 (右值) | int | int&& | 左值 | 右值 | print(int&&) |
总之一句话,T&& 保存原始值类别,std::forward 恢复原始值类别。
应用场景
- 泛型函数包装器(如
std::invoke) - 工厂函数(如
std::make_unique<T>(args...)) - 类型封装器(如
emplace_back,std::thread等)

浙公网安备 33010602011771号