熟悉完美转发失败的情形
本文最后更新于:April 20, 2022 am
本篇为介绍右值引用、移动语义和完美转发系列博客的第八篇,也是最后一篇,讨论完美转发失败的情形
右值引用,移动语义、完美转发系列目录:右值、移动语义和完美转发
在详细地讨论之前, 我们再来温习一下完美转发到底是什么。“转发”意味着——一个函数,将它的形参作为另一个函数的实参进行传递;“完美”则代表——让第二个函数接收到实参同第一个函数接收到的实参一致
值传递的函数自然不在讨论范围之内,因为两个函数的参数是两个不同的对象;因为我们不希望强迫外部使用指针,所以用指针传参的函数也同样不加以讨论。当讨论普遍意义上的转发时,我们实际上在讨论按照引用传参的函数
“完美转发”意味着,我们不仅仅在转发对象(传参),还附带了对象的显著特征——即它们是左值还是右值,以及它们是否带有const或者volatile修饰符
由前面几篇博客的讨论,我们知道只有当使用万能引用作为形参时,才有可能将之用作完美转发的源
当我们希望从函数fwd
转发一个参数到f
时,多半会这样写:
1 |
|
自然延伸一下,当需要转发任意多个任意类型的参数时,该怎么写呢?
这时,需要推展函数模板成可变参数模板
1 |
|
在其它标准库中,我们经常会看到它的影子:所有标准容器的emplacement
函数,还有智能指针的工厂函数,比如std::make_shared
, std::make_unique
(参见Item21:优先使用std::make_unique和std::make_shared
好了,最后我们再来明确一下,到底什么叫做“完美转发的失败”?在上述代码的基础上,我们规定:当用某一个参数X
调用函数f
后,f
函数的行为,同用此参数X
调用fwd
后函数f
的行为不一致时,完美转发失败。
大括号初始化物
假设函数f的签名如下:
1 |
|
我们可以这样来调用之f({1,2,3})
,编译器会将大括号隐式转换成std::vector<int>
但是如果向fwd
函数中传入相同的内容,则会导致编译不通过fwd({1,2,3})
这就是我们讨论的第一个完美转发失败的情形,实际上,所有的失败情形的原因都是一致的。直接调用函数f
时,编译器检查调用处的实参和函数签名中的形参,发现两者并不相匹配,于是将前者隐式转换成后者。在上述的例子中,编译器就将实参{1,2,3}
隐式转换成了一个std::vector<int>
类的临时对象,并让形参绑定到它
当通过调用fwd
函数来间接调用函数f时,编译器不会再检查fwd
函数实参的类型和fwd
函数的形参类型是否匹配,因为此函数的形参是万能引用;编译器会先推导出fwd
函数的万能引用的类型参数T
,然后检查类型参数T
和函数f
的形参类型,然后问题就发生了——编译器不知道应该将类型参数T
推导成什么
完美转发失败只会由以下两种原因之一导致:
编译器无法推导出某个类型参数
此例便是如此
编译器将某个参数的类型推导“错误”
这里的错误不单单指
fwd
函数实例化后,使用推导出的类型无法编译,还包含另一种情况,即后续调用函数f
时,f
的表现和我们所期待的不一致。比如,函数
f
有多个重载版本,随后编译器推导出的类型参数导致错误地调用了另一个版本的f
回顾上述的错误,调用fwd({1,2,3})
时,问题出在这里:我们向函数模板传入大括号初始化物,而函数模板的形参没有被声明为std::initializer_list
类型,这种情况按照标准的话来说,就是叫“非推导语境”。简单来说就是,由于fwd
函数的形参不是std::initializer_list
类型,编译器被禁止从大括号初始化物推导其类型参数,最终导致编译错误。
如果我们在外部初始化此变量,则后序的执行就可以正常进行
1 |
|
用0/NULL指代空指针
当向函数模板中传递0或者NULL来表明传递空指针时,编译器通常会错误地将类型参数推导为int,而非某个类型的指针,进而导致转发整形参数到子函数。
解决方案很简单,使用nullptr即可。
类中只声明的整形静态常量
通常情况下,如果某个类中有静态的常量( static const )变量,我们不需要单独对它们进行定义,声明就已经足够。这是因为编译器会对上述类型的变量进行常数传播( const propagation ),随后避免为它分配内存,举例而言
1 |
|
即使我们没有定义Widget::MinVals
,这段代码也能编译。编译器将所有出现Widget::MinVals
的地方都用它的值28来代替,如此一来不为它分配内存就是可行的。但如果代码中有地方试图去对Widget::MinVals
进行取地址,编译器就必须为它分配地址了,上述代码会在链接期间报错,因为变量Widget::MinVals
缺少定义
好,知道上述内容之后,我们再假设函数f
的签名为void f(std::size_t val);
如果我们直接调用f
,f(Widget::MinVals);
,没有任何问题
但如果通过函数fwd
来间接地调用f
,fwd(Widget::MinVals);
,代码会在链接时报错,并且提示Widget::MinVals
没有定义!
结合之前做的铺垫,是有什么东西取了它的地址吗?是的,尽管在源代码里没有出现类似&Widget::MinVals
的内容。
函数fwd
的参数是万能引用,而所有的引用,在编译器生成的目标代码中,都会被当做指针对待;而在二进制码中,以及在硬件层面,引用和指针没有任何区别。
在这一层面,正如那句老话所说,引用只不过是自动解引用(*
操作符)指针罢了。也正因如此,通过引用传递Widget::MinVals
和通过指针传递Widget::MinVals
都是一样的效率
所以说,要创建指向Widget::MinVals
的前提是,此变量必须有地址,底层的指针才能指向它
有一个问题,代码中的注释写的是“不应该”,而不是“不能”。这样说的原因是,根据标准,将Widget::MinVals
按引用传递就是必须要有它的定义,但是有一些实现没有强行要求这一点。根据编译器和链接器的不同,这份代码可能最终能够正常编译链接,但是毫无可移植性。
解决的方法很简单,在Widget.cpp
文件中加上定义即可
重载函数的名字和模板的名字
假设函数f
的参数是一个函数指针,并且我们希望f
利用这个函数做一些特定的工作,设f
的签名如下
1 |
|
现在,假设我们要传入的函数是processVal
,签名如下:
1 |
|
于是,我们可以这样调用f
:
1 |
|
即使此处的processVal
并不是个函数指针,而是两个重载函数的函数名。但是编译器知道通过f
的函数签名,来传入正确的processVal
地址
如果试图通过fwd
来间接调用函数f
,就会出现编译错误。fwd
作为函数模板,它的函数签名没有声明关于传入类型的任何信息,编译器无法通过fwd
的函数签名来决定应该传入哪一个processVal
函数的地址,于是编译错误!
1 |
|
如果将processVal
从重载函数换成函数模板,那么结局也会一样
1 |
|
这是因为,函数模板不代表某一个函数,它代表了许多函数
问题很明确,既然编译器不知道应该选择哪一个函数,那我们就可以通过手动指定的方式让编译器明白。如果我们希望传入参数是int
的processVal
的指针,我们就在外部声明它的函数指针,然后把函数指针传入函数fwd
1 |
|
当然,这要求我们提前知道fwd
内部调用的函数所需要的函数指针的类型。对于一个用于完美转发的函数模板,它的文档没有理由不记录这一点,毕竟这个函数模板可以接收任何类型的参数。
位域
最后一种失败案例和位域有关,当位域被用作函数参数时,完美转发失败。我们通过实际的例子来说明,观察下图的IPv4包头[1]:
1 |
|
假设函数f的签名如下
1 |
|
如果我们使用位域totalLength
来调用f
,没有问题:
1 |
|
但如果使用fwd
函数进行转发,则会导致编译错误。
1 |
|
fwd
的参数是引用,而且它是个non-const
的引用。
听起来问题好像不大,但C++标准明确规定了:禁止将non-const
的引用绑定到位域上
这样规定是有理由的,位域有可能由若干条二进制代码的碎片组成(比如,某个int的3-5位),但是我们无法直接地去访问这些位。早些时候,我们知道了指针和引用在硬件层面是一回事,正如我们无法创建指向位域的指针一样,我们也无法创建指向位域的引用(C++规定,开发者能操作的最小的内存存取单位是char
)
既然我们无法创建指向位域的指针或者引用,能传值的方式就是有两种了:值传递和const
的引用传递。
对于前者,实参被拷贝之后,位域其实是被存储到一个“对象”中的(标准规定,任意整形类型即可);对于后者,为什么能够创建位域的引用呢?其实答案已经呼之欲出了——编译器也是通过在函数中构造了一份实参的拷贝,然后让引用绑定到拷贝上。
既然如此,我们只需要在fwd函数外侧构建自己的拷贝,然后转发之
1 |
|
总结
在绝大多数情况下,完美转发就如同它的名字一般,完美地执行它的使命,我们也不需要对它做过多的关心。但当它不按套路出牌,使得看起来很正常的代码抛出了一堆堆编译错误——我们就很需要清楚它的瑕疵,以及如何应对相应情景。通常情况下,解决方案都是很直接的。
当类型推导失败,或者推导出了错误的类型时,完美转发失效
大括号初始化列表,用0或者NULL传参的空指针,仅有声明的static const成员变量,模板或重载函数的名字,以及位域
- 这里假设系统是小端。标准没有规定该使用大端还是小端,但编译器通常提供一种管理的方案供开发者选择 ↩
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!