-
完美转发(perfect forwarding),是指一个函数将其参数完整传递给另一个函数,使得第二个函数接受的对象与第一个函数完全相同,包括其类型、是左值还是右值、是否是
const
或volatile
的属性。因此我们这里讨论的只有万能引用参数,只有它们能携带这些信息。以下面这个函数为例:template
void fwd(T&& param) // accept any argument
{
f(std::forward(param)); // forward it to f
}
完美转发的目标简单来说就是希望以下两种调用的效果完全相同:
f( expression ); // if this does one thing,
fwd( expression ); // but this does something else, fwd fails
// to perfectly forward expression to f
-
本节下面就来讨论完美转发失效的几种情景。这些场景根本上是两类原因之一造成的:
- 编译器无法推断
fwd
的一个或多个参数的类型,导致编译失败。 - 编译器对
fwd
的一个或多个参数推断了错误的类型,这可能导致fwd
的调用编译失败,也可能导致使用错误类型参数调用f
的效果与用原类型参数调用f
效果不同。
- 编译器无法推断
-
第一种,使用大括号初始化。假设
f
的声明如下,在传参时使用大括号初始化的临时对象,那么f
和fwd
的调用结果为:void f(const std::vector& v);
f({ 1, 2, 3 }); // fine, “{1, 2, 3}” implicitly
// converted to std::vector
fwd({ 1, 2, 3 }); // error! doesn’t compile
后者调用失败。原因是当编译器能在调用现场看到形参类型时,它们会比较入参类型和形参类型,并尝试通过隐式转换使调用成功。f
的调用中,编译器会用 {1, 2, 3}
生成一个临时的 std::vecotr<int>
对象,调用成功。而 fwd
这样的调用情形被标准规定为不能进行类型推导的语境,于是调用失败。
这里的根本问题是 {1, 2, 3}
没有类型。因此,解决方案也很简单:先创建一个有类型的变量,再调用 fwd
即可。根据 Item 7,我们知道其类型应为 std::initializer_list<int>
。实际上即使你不知道,也可以借用另一个功能:auto
类型推导,来完成声明:
std::initializer_list<int> il = { 1, 2, 3 };
auto il = { 1, 2, 3 }; // 和上面二选一
fwd(il); // 调用成功,与 f({1, 2, 3}) 效果相同
-
第二种,使用
0
或NULL
作为空指针。Item 8 已经解释过将0
或NULL
作为空指针传给模板的隐患,因为编译器会把它们推导为整型(一般是int
)而不是一个指针类型。自然这种情况也会导致转发失效。解决方法很简单:使用nullptr
代替0
或NULL
。 -
第三种,仅声明(无定义)的
static const
数据成员。一般来说static const
成员仅需声明无需定义,因为编译器会对常量做优化(const propagation),将使用这类成员的地方直接换成常量,无需再为它们分配内存。例如:// Widget.h
class Widget{
public:
static const std::size_t MinVals = 28; // 仅声明无定义
// 注意,有初始化不等于有定义
};
// main.cpp
void f(std::size_t val); // f的声明f(Widget::MinVals); // 使用 MinVals,编译器会将其作为 f(28) 处理,ok
fwd(Widget::MinVals); // error,链接失败
直接调用 f
时编译器替换为 f(28)
处理没有问题,但 fwd
的参数是引用,引用在二进制码层面与指针相同,等于要对没有分配空间的 Widget::MinVals
取地址,导致链接失败。注意这种情况不一定适用于所有编译器,例如笔者测试上例在MSVC中能正常编译链接,但GCC链接失败。为了可移植性,我们还是应避免这种情况出现。解决方法也很简单:补上缺失的定义即可。
// Widget.cpp
const std::size_t Widget::MinVals; // 注意不要重复初始化,否则报编译错误
-
第四种,重载函数名或模板函数名。假设
f
接受的参数是一个函数指针:void f(int (*pf)(int)); // pf = “processing function”
void f(int pf(int)) // 这样声明也行
我们有两个重载版本的函数:
int processVal(int value);
int processVal(int value, int priority);
如果直接将其传递给 f
:
f(processVal);
这样的调用是成功的。虽然 processVal
只是一个函数名而非函数指针,但因为编译器在调用现场能看到 f
需要的函数的签名,它能找到正确的重载版本,并将其地址传递给 f
。然而对于 fwd
这个条件不成立,编译器不知道使用哪个重载,编译失败。模板函数也同理——它相当于一个无限种重载的函数。解决方法类似第一种情况:先声明一个对应 f
类型的函数指针,再用其调用 fwd
。
using ProcessFuncType = int (*)(int);
ProcessFuncType processValPtr = processVal;
fwd(processValPtr);
-
第五种,位域(Bitfield)。当需要控制每个 bit 的值或需要压缩存储空间时会使用位域(参考文档)。位域的成员作为参数传递给
fwd
时会出现问题:struct IPv4Header {
std::uint32_t version : 4,
IHL : 4,
DSCP : 6,
ECN : 2,
totalLength : 16;
};
void f(std::size_t sz);
IPv4Header h;
//处理h…
f(h.totalLength); // fine
fwd(h.totalLength); // error!
原因是这里 fwd
的参数类型是 non-const
的引用,而C++不允许将位域绑定到这样的参数上(不允许创建对位的指针)。类比 f
的成功调用,解决方法是提前手动将位域的值拷贝出来,然后传递该拷贝变量即可:
auto length = static_cast<std::uint16_t>(h.totalLength); // copy bitfield value
fwd(length); // forward the copy
总结
- 当模板类型推导失败,或推导出错误的类型时会导致完美转发失效。
- 会导致完美转发失效的五种入参类型为:大括号初始化对象,
0
或NULL
代表的nullptr
,仅声明的const static
数据成员,模板或重载函数名和位域。