C++完美转发
完美转发保留实参的值类别,使函数模板能正确传递左值或右值,通常结合 T&& 和 std::forward 使用。
C++完美转发
C++ 完美转发
完美转发是指:函数模板接收到的参数在传递给其他函数时,保持其原本的“值类别”(即左值或右值)。
- 常用于泛型函数模板中,用于“转发参数给其他函数”时,避免复制或不必要的拷贝构造。
- C++11 引入
std::forward
和&&
引用折叠来实现完美转发。
关键
使用万能引用:
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&&
,则结果为右值
即它使得模板参数保留了传入的原始“引用性质”。
右值引用对比万能引用
右值引用
1
2
int&& r = 5; // 合法,5 是右值
int&& r2 = x; // 错误,x 是左值
- 当类型是明确指定时,
int&&
就是右值引用。 - 只能绑定到右值(临时对象或
std::move
后的对象)。
万能引用
只有在模板类型推导时,形如 T&&
的参数被称为万能引用(Scott Meyers 在《Effective Modern C++》中提出的名词)。其行为如下:
传入参数的值类别 | 推导出的 T 类型 | 形参类型 T&& 实际类型 |
---|---|---|
传入左值 | T 被推导为 int& | 变成 int& && ,折叠为 int& (左值引用) |
传入右值 | T 被推导为 int | 仍是 int&& (右值引用) |
简而言之:万能引用会根据实参的值类别推导为左值引用或右值引用,达到“完美转发”的效果。
引用折叠规则
形式 | 折叠结果 |
---|---|
T& & | T& |
T& && | T& |
T&& & | T& |
T&& && | T&& |
所以当 T
推导为左值引用时,T&&
实际变成左值引用。
示例
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;
}
模板形参写成 T&&
,但这不是普通的右值引用,它是万能引用(转发引用)
传入左值时,模板推导时 T 会变成左值引用类型,比如
int&
。传入右值时,模板推导时 T 会变成普通类型,比如
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
,可以保持参数的原始值类别,避免值类别丢失。
重载解析和引用绑定规则
如果传的是一个左值,比如变量 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& | 是 | 否 |
常量左值引用 const T& | 是 | 是 |
右值引用 T&& | 否 | 是 |
按值参数 T | 是 | 是 |
重载解析会选最佳匹配,避免不必要拷贝。
本质
完美转发的关键目标是:尽可能将调用者的实参值类别(左值或右值)“原样”地传递给被调用者(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
等)
本文由作者按照 CC BY 4.0 进行授权