条款30:万能引用的完美转发失败情况
完美转发简介
- 完美转发的核心是将函数的参数“完美地”转发给另一个函数,保持参数的类型、左值/右值属性、
const
/volatile
属性不变。 - 万能引用(也称通用引用)使得模板函数能接受左值和右值参数,通过
std::forward<T>
保持实参特性传递。 - 例如:
1
2
3
4
| template<typename T>
void fwd(T&& param) {
f(std::forward<T>(param)); // 完美转发 param 给 f
}
|
1
2
3
4
| template<typename... Ts>
void fwd(Ts&&... params) {
f(std::forward<Ts>(params)...);
}
|
完美转发失败的根本原因
当使用万能引用转发时,编译器需要推导出正确的参数类型。推导失败或者推导出错误类型,就导致完美转发失败。这会引发两种情况:
- 无法编译(推导失败)
- 编译成功但运行行为与直接调用目标函数不同(推导类型不正确)
导致完美转发失败的典型实参种类
花括号初始化器(列表初始化)
1
2
3
4
| void f(const std::vector<int>& v);
f({1, 2, 3}); // 直接调用正常,隐式转换生成vector<int>
fwd({1, 2, 3}); // 错误!推导失败
|
- 原因:花括号初始化不属于类型推导上下文,模板参数无法推导为
std::initializer_list
,只能显示传入。 - 解决方案:
1
2
| auto il = {1, 2, 3}; // il的类型是 std::initializer_list<int>
fwd(il); // 完美转发成功
|
0 或 NULL 作为空指针
- 0 和 NULL 被推导为整型
int
,而非指针类型,导致推导失败。 - 解决方法:使用
nullptr
。
仅有声明的整型 static const
数据成员
1
2
3
4
| class Widget {
public:
static const std::size_t MinVals = 28; // 这是在类内声明并且初始化
};
|
- 这段代码中,
MinVals
是一个 类的静态常量整型成员,并且在类里直接赋值了 28
。 - 这种写法只是在类里声明并赋初值,但并没有在类外实际“定义”这个变量(即没有在某个
.cpp
文件里开辟内存空间)。
为什么这样写通常是没问题的?
- 编译器会在编译时把所有用到
MinVals
的地方,直接用常量 28
替换,也就是常量传播(const propagation)。 - 因此,如果代码里只是用它的值,比如:
1
| f(Widget::MinVals); // 编译器直接用 f(28) 替换
|
- 这种用法在编译时完全没有问题,链接时也没问题,因为没有真的访问
MinVals
的内存地址。
那为什么通过万能引用转发就会链接失败?
1
| fwd(Widget::MinVals); // 编译成功,但链接失败(未定义)
|
- 万能引用
T&&
是引用类型,引用底层的实现通常就是指针。 - 引用必须有一个真实的对象可以指向(即有内存地址)。
- 但是
MinVals
只是声明了,没有在类外“定义”,没有分配内存空间。 - 这时,代码编译没问题,但链接时找不到
MinVals
的存储空间,导致链接错误。
解决方法
- 需要在某个
.cpp
文件中为 MinVals
提供定义,但不要重复初始化(不能再次赋值):
1
2
| // Widget.cpp
const std::size_t Widget::MinVals; // 这里不需要写 = 28
|
- 这样会在程序中分配一块存储空间,引用时才有地址可指向,链接就没问题。
小结
- 静态常量整型成员如果只声明(类内初始化),并没有定义,不能用作引用参数。
- 直接用它的值(按值传递)没问题。
- 但引用它(如万能引用)需要定义才能链接通过。
- 所以如果打算传引用,必须在类外定义。
重载函数名和模板函数名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| void f(int (*pf)(int)); // 函数f,接受一个指向函数的指针,函数参数为int,返回int
int processVal(int); // 重载函数1,接受一个int参数
int processVal(int, int);// 重载函数2,接受两个int参数
f(processVal); // 直接调用f,编译器根据f形参类型自动选中重载版本processVal(int)
// 这里编译器知道f需要一个int(int)函数指针,选择正确的processVal重载
fwd(processVal); // 错误,推导失败,fwd是模板函数,无法推导出明确的processVal类型
// fwd模板接受任何类型,但processVal是函数重载名,类型不唯一,模板推导失败
template<typename T>
T workOnVal(T); // 函数模板,代表函数族(模板重载)
fwd(workOnVal); // 错误,推导失败,workOnVal是模板名,存在多个实例,无法推导出唯一类型
// 模板名不是具体函数,模板推导无法确定具体实例,导致完美转发失败
|
解决方法:显式指定函数指针或实例化模板函数
1
2
3
4
5
6
7
8
| using ProcessFuncType = int(*)(int); // 定义函数指针类型,指向接受int参数、返回int的函数
ProcessFuncType ptr = processVal; // 将processVal重载中匹配的那个函数赋值给函数指针ptr
fwd(ptr); // 通过fwd完美转发函数指针,成功,模板推导出明确类型
fwd(static_cast<ProcessFuncType>(workOnVal<int>)); // 对workOnVal<int>实例强制类型转换为函数指针类型,再完美转发
// 这样避免了模板名歧义,明确了类型,完美转发成功
|
- 使用具体函数指针类型消除了重载或模板名的不确定性。
- 明确类型后,
fwd
模板能正确推导参数类型,实现完美转发。
位域
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| struct IPv4Header {
std::uint32_t version:4, // 4位位域
IHL:4, // 4位位域
DSCP:6, // 6位位域
ECN:2, // 2位位域
totalLength:16; // 16位位域
};
void f(std::size_t sz); // 接收size_t类型参数,按值传递
IPv4Header h;
f(h.totalLength); // 直接调用,可以,因为传递的是totalLength的副本(按值传递)
fwd(h.totalLength); // 错误!fwd模板形参是通用引用(T&&),尝试绑定非const引用到位域
// 位域不是完整对象,不能绑定非常量引用,导致编译错误
|
- 原因:位域不可寻址,引用不可绑定非const位域。
- 位域本质上不是完整对象,不能绑定非常量引用。
- 按值传递(如f调用)是拷贝位域的值到普通变量,因此合法。
- 完美转发函数模板形参为引用,不能绑定非完整对象的位域。
- 解决方法:先拷贝位域到普通变量,再转发:
1
2
| auto length = static_cast<std::uint16_t>(h.totalLength); // 先将位域h.totalLength的值复制到一个普通的uint16_t变量length中
fwd(length); // 转发length,而不是直接转发位域,避免完美转发失败(因为位域不能被非const引用绑定)
|