💡 理解移动语义后,趁热打铁,继续理解完美转发。
在真正理解引用这篇 blog 中,我们在补充内容中简单地提到了转发引用和引用折叠的概念。
实际上,所谓的完美转发(Perfect Forwarding)就是基于转发引用(Forwarding references)、引用折叠(Reference collapsing)和类型推导(这不是这篇 blog 的重点)实现的。
那么,我们先回忆一下转发引用和引用折叠。
转发引用
**转发引用(Forwarding references)是一种特殊的引用,它保留了函数实参的值类别,从而使用std::forward转发它。**注意这句话中的隐藏信息:
- 转发引用与右值引用的地位是一样的,也是一种特殊的引用。
- 它保留了函数实参的值类别,注意是函数实参。
- 需要使用
std::forward来转发它。
按照这些信息,我们可以完善一下在之前文章中给出的示例:
1 | // 这里必须是函数模板,T 必须是模板类型参数 |
或者:
1 | // 这是 C++14 的特性 |
引用折叠
我们在之前的 blog 中简单的说过,引用折叠(Reference collapsing)是为了解决引用的引用是什么而存在的,并举了一个简单的例子:
1 | using T = int&; |
其实,这个例子只解释了一半。实际上,引用折叠要解决的问题有两个:
typedef或using自定义的类型形成的引用的引用,比如上面的例子。- 模板参数自动推导出来的类型形成的引用的引用,比如转发引用中的模板例子中的模板类型参数
T,编译器可能推导出&,也可能推导出&&。
对此 C++11 的解决方案就是引用折叠,对应的行为特征就是我们前面提到的表格,也一并抄过来:
| 模板类型 | T 的类型 | 最终类型 |
|---|---|---|
| T& | R | R& |
| T& | R& | R& |
| T& | R&& | R& |
| T&& | R | R&& |
| T&& | R& | R& |
| T&& | R&& | R&& |
在回顾完之前的内容后,我们再开始看完美转发。🧐
为什么需要完美转发?
还是一样,我们需要先思考一下为什么需要完美转发。其实,答案就是为了将传递给函数模板的函数实参以其原本的值类别转发给其他函数。
下面我们举一个简单的例子,逐步分析。一开始,我们的需求是这样的:
1 |
|
在上述代码中,我们有一个函数func和它的重载版本,不同之处在于一个是以左值引用为参数,另一个是以右值引用为参数,期望输出传入值的值类别。然后,我们用类似的方法得到了两个相似的函数且也构成重载的同名函数dofunc,作用是来调用func。
使用模板简化
可是,上述代码的输出结果并不符合我们的预期,而且写了很多不必要的重复代码,我们可以先考虑使用模板来简化,并结合我们上面提到的转发引用,就可以得到:
1 |
|
在上述代码中,我们把dofunc函数变为了一个函数模板,这样即便有新的func函数,我们也不需要改动dofunc。可最关键的问题——输出结果不合预期,仍然没有解决。
问题产生的原因
观察输出结果,我们发现,不管传入的是左值i还是纯右值10,最终输出的结果都是lvalue。这意味着,我们最终只调用了void func(int& i)这个函数。可我们明明给函数传递的值类别是正确的啊?为什么不会调用呢?
实际上,我们唯一能确定的就是值类别一定发生了改变,且都成了左值,所以都调用的第一个func函数。
现在我们来分析究竟是哪里发生了改变,首先我们知道,这里是一个函数模板,模板会自动推导参数的类型。在上面的例子中:
1 | dofunc(i); // 会把 T 推导为 int& |
关于模板参数类型自动推导的话题,这里不做深究,我们只需要知道**编译器在做类型推导时,会将绑定左值的 T 推导为左值引用类型,将绑定右值的 T 推导为原基本类型。**那么,最终我们的函数模板中i的类型就是:
1 | dofunc(i); // T = int&, i = int& && |
再按照我们上面提到的引用折叠的规则,继续推导可得:
1 | dofunc(i); // T = int& => i = int& && = int& |
看着好像很完美,最终一个类型是左值引用,一个是右值引用,按理说应该是可以输出符合预期的结果的。为什么还是有问题呢?再回顾前面讨论的左值与右值,我们知道由右值引用变量组成的表达式也是左值表达式,所以我们在dofunc中传递给func的右值引用变量依然是一个左值,最终调用的还是左值版本的func。
现在,问题我们已经清楚了,如何解决呢?
解决问题
答案很简单,就是使用std::forward,正如我们在转发引用中举的例子那样:
1 |
|
现在,我们就可以得到符合预期的结果了。🎉
而且,回过头来思考,我们那个没有使用模板的例子,其实也是因为值类别不匹配导致的错误结果,我们也可以做一些简单的修改,从而得到正确的结果:
1 |
|
在上述代码中,我们分别std::move和std::forward得到了预期结果,这两种方式都可以。同时,这两个函数的用法,目前我们也都有了一定的认识,就不再赘述了。
到此,我们的问题就算是完全解决了。😴
总结
现在再来总结一下所有的内容:
- 我们明白了完美转发至少由三部分构成:转发引用、引用折叠和
std::forward。 - 我们明白了转发引用、引用折叠的由来和用途,知道了这俩究竟是什么。
- 我们明白了完美转发的用途(或者说意义),那就是将传递给函数模板的函数实参以其原本的值类别转发给其他函数。
好了,到此为止,这篇 blog 到这里就结束了,理论已经分析完毕,剩下的就是实践了。