《Effective Modern C++》学习笔记 - Item 30: 了解完美转发失效的场景

  • 完美转发(perfect forwarding),是指一个函数将其参数完整传递给另一个函数,使得第二个函数接受的对象与第一个函数完全相同,包括其类型、是左值还是右值、是否是 constvolatile 的属性。因此我们这里讨论的只有万能引用参数,只有它们能携带这些信息。以下面这个函数为例:
template<typename T>
void fwd(T&& param) 			// accept any argument
{
	f(std::forward<T>(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
  • 本节下面就来讨论完美转发失效的几种情景。这些场景根本上是两类原因之一造成的:
    1. 编译器无法推断 fwd 的一个或多个参数的类型,导致编译失败。
    2. 编译器对 fwd 的一个或多个参数推断了错误的类型,这可能导致 fwd 的调用编译失败,也可能导致使用错误类型参数调用 f 的效果与用原类型参数调用 f 效果不同。

  • 第一种,使用大括号初始化。假设 f 的声明如下,在传参时使用大括号初始化的临时对象,那么 ffwd 的调用结果为:
void f(const std::vector<int>& v);

f({ 1, 2, 3 }); 	// fine, "{1, 2, 3}" implicitly
 					// converted to std::vector<int>
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}) 效果相同
  • 第二种,使用 0NULL 作为空指针。Item 8 已经解释过将 0NULL 作为空指针传给模板的隐患,因为编译器会把它们推导为整型(一般是 int)而不是一个指针类型。自然这种情况也会导致转发失效。解决方法很简单:使用 nullptr 代替 0NULL

  • 第三种,仅声明(无定义)的 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

总结

  1. 当模板类型推导失败,或推导出错误的类型时会导致完美转发失效。
  2. 会导致完美转发失效的五种入参类型为:大括号初始化对象,0NULL 代表的 nullptr,仅声明的 const static 数据成员,模板或重载函数名和位域。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值