前言
完美forward是C++11的一大特性,但实际上在某些场合forward并不完美。
完美转发
在提及完美转发之前,首先回顾一下转发的定义:一个函数将其参数传递给另一个函数,但关键在于第二个函数接受到的对象必然需要和第一个函数发出的是同一个。这直接排除了传值,因为传值传递的都是副本。指针也被排除,因为我们并不希望调用者试图传递指针。事实上,我们提到的转发一般只与引用有关。
完美转发意味着我们不仅仅转发对象,我们还转发它们的显著特征:type,左右值,以及const或volatile,这直接表明我们只能用universal reference,因为只有它能够对传入参数的左右值加以编码区分。
假设当前有一个转发实例如下:
1 | template<typename T> |
转发函数在本质上具备通用性。例如,fwd模板接受任何类型的参数,并且它会转发任何类型的参数。这种通用性的逻辑扩展是:转发函数应当成为一个接受可变参数的函数模板,例如fwd的扩展形态应当为:
1 | template<typename... Ts> |
这种写法广泛见于智能指针工厂函数、std::make_shared、std::make_make_unique。
我们所说的完美转发失败指的是:接受同样的参数,f能够正确执行,fwd则表现地与f有所不同:
1 | f( expression ); // if this does one thing, |
有些参数将造成这些现象,下文将依次探讨并给出解决方案。
Braced initializers
假定函数有声明如下:
1 | void f(const std::vector<int>& v); |
在本案例中,当传入参数为大括号初始化器时,f与fwd表现不同:
1 | f({ 1, 2, 3 }); // fine, "{1, 2, 3}" implicitly converted to std::vector<int> |
原因在于大括号初始化器是完美转发的一大失败案例。
在f中,编译器明确了解f需要接受的对象,因此编译器将通过{1,2,3}生成一个临时对象vector,将v绑定至其上。但fwd将推导出{1,2,3}的类型,并将其与f声明的形参相比较,完美转发将会在以下两点情况发生时失效:
- 类型推衍失败
编译器无法推断出fwd接收参数的类型,编译失败。 - 类型推衍错误
这将触发f接纳了编译器推导得到的错误类型,更进一步地触发f函数的某些重载版本。
在上述调用中,fwd无法推衍出{1,2,3}的类型,因为template并不能推衍出initializer_list,因此触发编译错误。
解决方案在Item2中即有所提及:auto能够推衍得到正确的initializer_list,因此我们可以:
1 | auto il = { 1, 2, 3 }; // il's type deduced to be std::initializer_list<int> |
0 or NULL as null pointers
在Item8中我们曾经提及,当你传入0或NULL时,编译器将认为它们是一个整形数据而非一个指针,因此转发它们则等价于转发错误的类型(你原本想转发一个指针类型),解决方法十分简单:以nullptr代替它们。
Declaration-only integral static const data members
一般来说,我们并不需要在类中定义整型静态const数据成员,单单写出声明足以。原因在于编译器将对这些成员的值执行常量传播,因此无需为它们留出内存,考虑如下代码:
1 | class Widget { |
编译器将会在所有出现MinVals的地方将其替换为28,但如果当前需要采用MinVals的地址(例如创建了一个指向MinVals的指针),那么MinVals将需要存储(以便指针指向某个东西),上述代码能够编译通过,但会在链接期报错。
那么,假设dangqian函数f被声明为:
1 | void f(std::size_t val); |
那么使用fwd又会导致完美转发失效:
1 | f(Widget::MinVals); // fine, treated as "f(28)" |
虽然源代码中没有提及MinVals的地址,但fwd的参数是一个universal reference,编译器在生成代码时通常将引用视作指针,在程序的底层二进制码(以及硬件)中,指针和引用本质上是相同的,但我们并没有在内存中存储MinVal,因此导致了链接期失败。
解决方法很简单,我们不仅仅声明,而且去定义MinVal,这样便可以通过引用转发integral static const data member:
1 | const std::size_t Widget::MinVals; // in Widget's .cpp file |
值得注意的是该定义并不执行任何初始化操作。
Overloaded function names and template names
假定当前f形参为某个函数指针:
1 | void f(int (*pf)(int)); // pf = "processing function" |
当然,我们也可以用另一种语法来完成声明:
1 | void f(int pf(int)); // declares same f as above |
更进一步地,假设当前存在重载函数processVal如下所示:
1 | int processVal(int value); |
我们可以很自然地将其传递给f:
1 | f(processVal); // fine |
显然,传递至f是参数只有int的重载版本。
但如果将processVal传递给fwd,后者将会产生困惑:究竟传递哪个函数?
1 | fwd(processVal); // error! which processVal? |
函数模板自然也存在同样的问题,它并不代表某个函数,而是许多函数:
1 | template<typename T> |
解决方案十分显然,手动明确传入的是何种重载或实例化即可。例如:
1 | using ProcessFuncType = int (*)(int); |
Bitfields
假定ipv4头可被建模如下:
1 | struct IPv4Header { |
若f当前形参为size_t类型,那么以totalLength作为实参传入完全没毛病:
1 | void f(std::size_t sz); // function to call |
但fwd则可耻地失败了:
1 | fwd(h.totalLength); // error! |
原因在于fwd的参数是一个引用,而h.totalLength是一个non-const bitfield。C++明确声明:“non-const引用禁止绑定至bitfield。”位域可以由机器字的任意部分组成(例如32位int的3-5位),但我们无法获取指向它们的指针,因此引用绑定到bitfield自然也不行。
解决方案是:创建一个副本以存储Bitfield的值,然后将其传递给fwd:
1 | // copy bitfield value; see Item 6 for info on init. form |