C++完美转发
完美转发保留实参的值类别,使函数模板能正确传递左值或右值,通常结合 T&& 和 std::forward 使用。
C++ 完美转发
完美转发 是指:函数模板接收到的参数在传递给其他函数时,保持其原本的“值类别”(即左值或右值)。
- 常用于 泛型函数模板 中,用于“转发参数给其他函数”时,避免复制或不必要的拷贝构造。
- C++11 引入
std::forward
和&&
引用折叠来实现完美转发。
完美转发的关键
- 使用 万能引用(Forwarding Reference):
1
2
template<typename T>
void func(T&& arg); // T&& 是万能引用
搭配
std::forward<T>(arg)
保留参数的值类别1
std::forward<T>(arg);
根据 T 的类型判断是否将
arg
保持为右值:- 如果
T
是int&
,则结果为左值 - 如果
T
是int&&
,则结果为右值
即它使得模板参数保留了传入的原始“引用性质”。
- 如果
右值引用对比万能引用
右值引用(Rvalue Reference)
- 当类型是明确指定时,
int&&
就是右值引用。 - 只能绑定到右值(临时对象或
std::move
后的对象)。
1
2
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 完美转发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#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;
}
输出:
1
2
3
4
左值引用
左值引用
左值引用
右值引用
重载解析和引用绑定规则
如果传的是一个左值,比如变量 x
。调用 print(x)
,有三个版本:
1
2
3
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
1
2
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
就像是“值类别的还原器”。
对比总结
1
传入实参 --> 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
等)