Item 30: Familiarize yourself with perfect forwarding failure cases.

Effective Modern C++ Item 30 的学习和解读。

完美转发(perfect forwarding)是 C++11 非常重要的一个特性。转发意味着一个函数将其参数传给另一函数,第二个函数的目的是接收第一个函数接收到的参数,并且二者是同一个对象。这就排除了值传递参数形式,因为值传递需要拷贝对象,拷贝后对象就不是源对象了。指针传递也可以达到转发的效果,但要求用户必须传递指针,算不上完美转发。

完美转发不仅要转发对象本身,还有附带属性,比如对象是左值还是右值、是 const 还是 volatile。根据 Item 24 的介绍,只能使用万能引用的方式,因为只有万能引用能够对传递的参数的左值或右值信息进行编码。

典型的形式如下:

template<typename T>
void fwd(T&& param)           // accept any argument
{
  f(std::forward<T>(param));  // forward it to f
}

fwd 还可以转发可变参数:

template<typename... Ts>
void fwd(Ts&&... params)           // accept any arguments
{
  f(std::forward<Ts>(params)...);  // forward them to f
}

一些类型的参数会导致完美转发失败,本 Item 接下来介绍几个导致完美转发失败的场景。

花括号初始化(统一初始化、列表初始化)

假设函数 f 申明如下:

void f(const std::vector<int>& v);

使用统一初始化方式调用 f 没有问题:

f({ 1, 2, 3 }); // fine, "{1, 2, 3}" implicitly
                // converted to std::vector<int>

对于 f ({ 1, 2, 3 }),在函数调用点,编译器会比较入参和函数的参数申明,看它们是否兼容。如有必要,编译器会执行隐式类型转换来让调用成功。在这个例子中,编译器会将 {1, 2, 3} 转换成一个 std::vector<int> 类型(因为 std::vector 有初始化列表的构造函数版本),这样就调用成功了。

但是将列表初始化参数传递给 fwd,则产生编译错误:

fwd({ 1, 2, 3 }); // error! doesn't compile

通过转发函数模板 fwd 间接调用 f,编译器将不再比较通过 fwd 调用点传入的参数和函数 f 的参数申明。取而代之的是,编译器推导通过 fwd 传递的参数类型,并将比较推导类型和函数 f 的参数申明。下面两者之一的情况发生时,将导致完美转发失败:

  • 编译器无法推导 fwd 的参数类型。
  • 编译器将 fwd 的参数类型推导“错误”。这里的错误可能是使用推导类型的 fwd 的实例无法编译通过,也可能是使用推导类型调用 f 和直接使用传递给 fwd 的参数调用 f 的行为不一样。

对于 fwd({ 1, 2, 3 }),由于 fwd 没有申明为一个 std::initializer_list,编译器不会将表达式 {1, 2, 3} 推导类型,因此编译失败。

#include<iostream>
#include<vector>

void f(const std::vector<int>& v) {
  // do something
}
  
template<typename T>
void fwd(T&& param)           // accept any argument
{
  f(std::forward<T>(param));  // forward it to f
}
 
int main () {
  fwd({1, 2,3});
}
// 编译报错如下:
main.cpp: In function 'int main()':
main.cpp:15:6: error: no matching function for call to 'fwd(<brace-enclosed initializer list>)'
   15 |   fwd({1, 2,3});
      |   ~~~^~~~~~~~~~
main.cpp:9:6: note: candidate: 'template<class T> void fwd(T&&)'
    9 | void fwd(T&& param)           // accept any argument
      |      ^~~
main.cpp:9:6: note:   template argument deduction/substitution failed:
main.cpp:15:6: note:   couldn't deduce template parameter 'T'
   15 |   fwd({1, 2,3});

这是完美转发失败的第一个例子。不过,上面的问题也可以解决:虽然模板类型推导无法推导出初始化列表的类型,但是 auto 可以:

auto il = { 1, 2, 3 }; // il's type deduced to be
                       // std::initializer_list<int>
fwd(il);               // fine, perfect-forwards il to f

0 或 NULL 作为空指针

Item8 中介绍过, 当使用 0 或者 NULL 作为一个空指针传给模板,会被推导成 int 类型,无法被当成指针类型进行完美转发。

#include<iostream>

void f(void*) {
  // do something
}
  
template<typename T>
void fwd(T&& param)           // accept any argument
{
  f(std::forward<T>(param));  // forward it to f
}
 
int main () {
  fwd(NULL);
  // fwd(0);
}

// 编译报错
main.cpp: In instantiation of 'void fwd(T&&) [with T = long int]':
main.cpp:14:6:   required from here
main.cpp:10:4: error: invalid conversion from 'long int' to 'void*' [-fpermissive]
   10 |   f(std::forward<T>(param));  // forward it to f
      |   ~^~~~~~~~~~~~~~~~~~~~~~~~
      |    |
      |    long int
main.cpp:3:8: note:   initializing argument 1 of 'void f(void*)'
    3 | void f(void*) {
      |        ^~~~~

解决办法也很简单,使用 nullptr 作为空指针。

仅仅声明整型的静态常量(static const)数据成员

一般地,没有必要在类中定义静态常量数据成员,仅仅申明它就够了,因为编译器会对执行常量传播,因此不会为静态常量数据成员分配实际的存储空间。例如下面的代码片段:

class Widget {
public:
  static const std::size_t MinVals = 28; // MinVals' declaration};// no defn. for MinVals

std::vector<int> widgetData;
widgetData.reserve(Widget::MinVals); // use of MinVals

虽然 Widget::MinVals 没有存储空间,但是使用它初始化 widgetData 是没有问题的,因为编译器会直接将使用到它的地方替换成 28。但是若对 Widget::MinVals 取地址,将找不到 Widget::MinVals 的定义,链接时将会失败。

void f(std::size_t val);
f(Widget::MinVals);   // fine, treated as "f(28)"
fwd(Widget::MinVals); // error! shouldn't link

虽然万能引用没有对 Widget::MinVals 取地址,但是万能引用的参数是引用类型,对于编译器而言,引用通常被对待成指针一样。因此,完美转发也就失败了。

上面说的是引用通常被当成指针,不排除有的编译器不是这样,也即可以对静态常量数据成员进行完美转发。但我们没必须要冒这样的险,只要增加一个定义即万事大吉。

const std::size_t Widget::MinVals; // in Widget's .cpp file

函数重载和函数模板

void f(int (*pf)(int)); // pf = "processing function"

int processVal(int value);
int processVal(int value, int priority);

f(processVal);   // fine
fwd(processVal); // error! which processVal?

对于重载函数 processVal,通过 fwd 转发 processVal 将会失败,因为模板类型推导无法推导 processVal 的类型。使用模板函数,也有同样的问题。

template<typename T>
T workOnVal(T param) // template for processing values
{}
fwd(workOnVal); // error! which workOnVal
                // instantiation?

解决的办法是:主动给出函数重载和函数模板的类型:

using ProcessFuncType = // make typedef; see Item 9
  int (*)(int);           
ProcessFuncType processValPtr = processVal;  // specify needed signature for processVal
fwd(processValPtr); // fine
fwd(static_cast<ProcessFuncType>(workOnVal)); // also fine

当然上面的行为有点奇怪,完美转发需要知道转发的是哪一个。

位域

完美转发失败的最后一个例子是使用位域作为函数参数。

struct IPv4Header {
  std::uint32_t version:4,
                IHL:4,
                DSCP:6,
                ECN:2,
                totalLength:16;};

void f(std::size_t sz); // function to call
IPv4Header h;f(h.totalLength); // fine
fwd(h.totalLength); // error!

失败的原因是因为 fwd 的参数是一个引用,C++ 标准规定一个非 const 引用无法引用一个位域字段。这样的规定也是合理的,比特位域通常只是 int 类型的一部分,没有一个确切的地址,也就没办法通过指针指向它,而通常引用本质上是指针,因此无法引用位域。

而指向常量的引用可以绑定到位域,本质上是因为绑定到了位域的一个拷贝对象上(比如 int)。

#include<iostream>

struct IPv4Header {
  std::uint32_t version:4,
                IHL:4,
                DSCP:6,
                ECN:2,
                totalLength:16;
};

void f(const int&) {
  // do something
}

template<typename T>
void fwd(T&& param)           // accept any argument
{
  f(std::forward<T>(param));  // forward it to f
}
  
int main () {
  IPv4Header h;
  f(h.totalLength);  // fine
  fwd(h.totalLength); // error
}

// 编译报错
main.cpp: In function 'int main()':
main.cpp:24:9: error: cannot bind bit-field 'h.IPv4Header::totalLength' to 'unsigned int&'
   24 |   fwd(h.totalLength);
      |       ~~^~~~~~~~~~~

那么完美转发位域也可以通过对位域进行拷贝,然后再转发。

// copy bitfield value; see Item 6 for info on init. form
auto length = static_cast<std::uint16_t>(h.totalLength);
fwd(length); // forward the copy
  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值