欢迎光临散文网 会员登陆 & 注册

深入浅出C++:完美转发不完美

2022-06-08 12:08 作者:飞翔の回声_FlappyEcho  | 我要投稿

    上一次文章里,我在梳理了以std::move为基础的移动语义的场景和概念。

深入浅出C++:移动语义不移动 - 哔哩哔哩 (bilibili.com)

    在这一个专栏里,我将探讨完美转发,在加入了针对完美转发的叙述以后,理论和概念层面针对C++11移动语义的讨论就可以基本结束了。

    我仍然会结论先行:

    所谓完美转发,其实只是根据【不同的情况】传递左值引用或者右值引用两种情况以精准调用相应的函数重载。而所谓【不同的情况】,则根据std::forward<T>的模板参数T来决定,可以简单的认为,在考虑引用折叠的情况下,当T推导出来的类型带有奇数个&时,转发左值引用,偶数个(包括0)时转发右值引用。

    好了,结论说完了,如果有些同学可以理解我所说的结论,并且表示大大的认可,你们就已经可以退出这篇文章去寻找对自己更有意义的内容去了。

    而那些抱有不同观念的人以及并不能理解的同学们,则可以开始阅读我接下来的说法,或是尝试去理解,或是去思考与我辩论的说辞,我十分欢迎有同学愿意以私信或者评论的方式与我讨论这些内容。

    在上次针对std::move的讨论中,我将左值与右值的基础概念扔给了cppreference,我可以接受你们对使用std::move和std::forward存在疑惑,但我确实需要你们具备识别左值和右值的能力。

    最基本的。

    在上述的代码中,我需要你能知道,因为test是一个左值,所以第一个Test的调用回事参数为const Data&的版本,而Data(12)并没有一个拥有名字的变量来接住它,它的生命周期仅在这一行代码,故而它是一个右值,因此会调用到Data &&的Test重载版本。

    在各位能区分左值和右值的情况下,我们继续。

    我相信各位能够通过各种途径来查到std::move和std::forward的源码,亦或是相近的实现,我在这里随便贴出来一个。

    C++标准库的很多工具的实现,真的是体现了一个大道至简的真理。包括std::enable_if也一样,这些在应用中能解决开发者很多问题的东西,实现起来确实相当的简单。甚至在你使用enable_if的时候,你自己需要往模板里塞的东西要比enable_if自己的实现还要多。

    当然上述的话属于是闲聊,这段代码理解起来不难(额,好吧,哪怕写的挺短但看上去还是挺不符合平日里应用程序的编码的,但那不重要),我也并不打算讲这段代码,而你只需要关注到那个static_cast就足够了。这个东西是什么?就是一个类型转换而已。

    想想咱们写的两篇文章的名字,移动语义不移动,完美转发不完美。

    这足以说明:std::move和std::forward就是两个【能够根据不同情况】而进行的【强制类型转换】,为的就是我先行结论中的精准调用相应的函数重载。

    功能上,std::forward甚至能够完美取代std::move,只要你不怕多打那近乎一倍的字,虽然从标准库的作者角度上来说,他们就是希望我们针对两种不同的语境来使用这两个不同的工具。

    如此看来:

    std::move能够将任何形式的引用以右值引用返回,
    std::forward能够将任何形式的引用以其原本的引用形式返回(只能限定为左值引用或者右值引用)。

    我们知道,函数传参有3中形式,按引用,按值和按指针。假如你能够深刻的理解所谓【指针也是一个变量】的话,就会明白,按值传递和按指针传递的本质是一样的。通过上述总结,咱们应该清楚,完美转发是不能够按值转发的,这是其不完美的其中之一。

    考虑如下代码:

    如上的代码使用g++编译器编译运行后,结果如图:

代码运行结果

    我将运行结果对应注释在了函数边上。

    而且我们可以看到,最终调用的是形参为Data &的函数而非const Data&,对于并非const Data类型的对象test而言,这无可厚非。

    而我如果将这个地方展开来说,就会涉及一些关于模板的坑,这个坑可以用一句话来总结就是:因为重载决议的缘故,导致你想要调用的【可调用对象】和编译器实际调用的【可调用对象】不一样

    其中,对于函数形参的类型有没有const等关键字来修饰,会是重载决议考虑的范畴。

    而针对这个知识点,Effective Modern C++ 的条款26和27有非常详细的展开,让我来简述肯定是不如专家解释的靠谱的。

    针对这段代码,我们可以知道,std::move无论怎么折腾,只会返回右值引用,std::forward则会根据传给它的模板参数不同而根据不同情况返回左值引用或右值引用,无论怎么说,它们返回的都是引用,绝对不是值。

    你不能够干出写一个值传递的函数,然后接收一个std::forward转发的对象就说它能够传递值,那只是函数的形参以实参作为【引用参数】进行了一个拷贝。

    我们明确了完美转发只会转发引用后,再考虑现实的应用场景,可以总结出:当我们使用完美转发的时候,几乎都是配合着模板和万能引用来使用的。

    原因是:万能引用会让形参最终都会被推导为引用类型的数据,而模板则需要供std::forward来根据类型进行精准的引用转发使用。

    看一个简单的代码,它还是我从Effective Modern C++上抄来的。

     配合着T&&,param这个形参无论如何都会被编译器推导为引用,这个时候你需要考虑引用折叠。

    在logAndProcess(w);这段代码里,w是左值,param则被推导为Widget&,则表明,Widget& 与 T&&是等价的。这个时候,T就会根据引用折叠被推导为Widget&类型。
    (当T为Widget&时,T&&对应Widget&&&,引用折叠可以消去【2的倍数个&符号】,直到【类型最后后面的&符号数量】 <= 2,故Widget&&&可以消去两个&,转成Widget&型与w的类型相匹配。)

    如果还不懂,我们再看logAndProcess(std::move(w));这段代码,param对应的必然是一个Widget&&,则T&&与Widget&&等价,还用我说T是什么类型吗?T会被推导为Widget类型。

    而std::forward<Widget>(param)和std::forward<Widget&>(param)会出现什么结果早在之前的代码演示中我们就给出了答案,还记得我们先行的结论吗:

    可以简单的认为,在考虑引用折叠的情况下,当T推导出来的类型带有奇数个&时,std::forward转发左值引用,偶数个(包括0)时,std::forward转发右值引用。

    这个简单但是神奇模板应用最终让std::forward根据模板参数T的类型转发相应的引用,而结果上与形参param的类型结果完美匹配。

    还记的本文叫什么吗?完美转发不完美。并不完美的完美转发其实还有其他不完美的地方,在某些奇怪的场景下,它会类型推导错误进而导致执行失败,而这一点在Effective Modern C++的条款30也有十分详细的说明,才疏学浅且并没有为此准备的我就不在本文展开了。

    而我要表达的核心内容,到此为止就算是全部结束了。

    考虑到我对于移动语义大部分的理解和思考已经在上一篇文章中全部表达了,配合着std::forward的此文仅仅是将移动语义的基础知识面说全了而已,因此本文的内容并不像之前那么多。

    如果您觉得我写的文章对您有帮助亦或是想与我交流,都欢迎您在评论区留言。如果您对其他的知识点或是语言特性感兴趣,也可以尝试着让我知道,若是我自己达到了能够分享该知识点的水平,便会想办法在日后继续写相关文章。

    最后,十分感谢您的阅读,我们有缘再见。

    


深入浅出C++:完美转发不完美的评论 (共 条)

分享到微博请遵守国家法律