C++11 图说VS2013下的引用叠加规则和模板参数类型推导规则
背景:
最近在学习C++STL,出于偶然,在C++Reference上看到了vector下的emplace_back函数,不想由此引发了一系列的“探索”,于是就有了现在这篇博文。
前言:
右值引用无疑是C++11新特性中一颗耀眼的明珠,在此基础上实现了移动语义和完美转发,三者构成了令很多C++开发者拍案叫绝的“铁三角”(当然不是所有C++开发者)。而在这个“铁三角”中,有一个无法回避的关键细节,那就是引用叠加规则和模板参数类型推导规则。其实,关于这两个规则,可查到的资料不少,但都有一个特点——简单(就形式而言)而难懂(就理解而言)(起码在下这么认为),而且,都没有例证,仅仅是简明扼要地交代。而本文恰恰是将这一细节展开,给出演示和证明。诚然,这不是什么开创性的工作,但在下认为也是必不可少的,因为它让人们对这一关键细节了解得更加深入和透彻,另外,从某个角度来说,也填补了空白。
“图说”是因为:有图有真相,一目了然,真真切切,不容辩驳。
“VS2013下”是因为:本文所有测试和截图都来自VS2013,考虑到不同编译环境下结果可能会略有不同,所以,严谨起见,这里加了“VS2013下”。
最后,再说两点:
1.本文的行文形式(也可以说是逻辑顺序):先结论,再证明,必要时加以解说。
2.本文使用了大量的截图,所以读起来可能会有一种连篇累牍之感(但实际上文章逻辑结构清晰,内容一目了然),给读者带来的阅读上的不适,敬请谅解。
参考资料:
1.维基百科.右值引用 地址:http://zh.wikipedia.org/wiki/%E5%8F%B3%E5%80%BC%E5%BC%95%E7%94%A8(强烈建议大家看)
2.聚客频道.[C++] 右值引用:移动语义与完美转发 作者:Dutor 地址:http://ju.outofmemory.cn/entry/105978
3.博客园.【原】C++ 11完美转发 作者:Hujian 地址:http://www.cnblogs.com/hujian/archive/2012/02/17/2355207.html
4.IBM developerWorks.C++11 标准新特性: 右值引用与转移语义 作者:李胜利 地址:http://www.ibm.com/developerworks/cn/aix/library/1307_lisl_c11/
正文:
好了,书归正文。
为把问题说清楚,我们先给出以下函数:
template<typename T> void f(T&& fpar)//formal parameter 形参 { //函数体 } //调用 int a=1; int& apar=a;//actual parameter 实参 f(apar);
在此基础上,给出以下表格(设A为基本类型,比如int):
表1
说明:
1.在前面的代码中,调用前形参fpar被声明的类型是T&&,调用时传入的实参apar的类型是int&。
2.上表中,2、3、4列对应了引用叠加规则,2、3、5列对应了模板参数类型推导规则。
3.由上表可以知道:
引用叠加规则的规律是:调用前fpar与apar中有一个是&,结果(即调用后fpar的实际类型)就是&;只有当fpar与apar都是&&时,结果才是&&。
模板参数类型推导规则的规律是:只有调用前fpar是&&,apar是&时,调用后T的实际类型才是A&,其余3种情况下都是A。(仅就上表,许多资料上不上这样,原因
在于红色部分不一样,见下面的说明4)
4.注意到上表中红色的A,在查阅过的资料中,那个位置是A&,但在下得到的结果却是A,后面会详细解释。
5.本文所讨论的模板参数类型推导,仅是针对上面例子中的T而言的,在C++11里,更经典的类型推导包括auto,decltype等。
下面逐一给出验证与说明:
1.验证规则1
看图:
图1
程序中我们设断点监视变量,我们看到,ra作为int&类实参调用函数wai(因为是外层函数,这里简单命名为wai,不影响说明问题),调用后,T& w_a变成了int& w_a(即实际类型成了int&),而T w_aa成了int型,即T的类型是int型。这里,调用后形参w_a的实际类型满足引用叠加规则1(上表中的)。
关于引用叠加,有两种理解方式(以上例为例说明):
方式一:
参数传递时,T&与int&“作用”,结果是int&,即T&+int& -> int&。我们将其视为规定,不必解释。(上表正是以这种方式给出的)
方式二:
参数传递时,将实参ra前面的int&传给T(即将T换成int&),于是,int& & -> int&(注意int& &的两个‘&’间有空格,不是右值引用),而将int& & ->
int&视为规则。基于方式二,上表将变成(不考虑调用后T的类型):
表2
其中,第1个”加数“是将T换成的内容,也就是实参前的类型,第2个”加数“是函数参数列表中T后的引用形式,”和“是函数调用后形参的实际形式。下面图说方式二中规定的正确性:
A& & -> A& A& && -> A& A&& & -> A& A&& && -> A&&
两种方式都可以。只不过在下觉得,方式二绕一点,并且,有一种T先变成int&(以图1所示为例),然后又变成int的莫名其妙之感。所以,在下推荐方式一。
在T的推导上,我们采用这样的方式:先由叠加原理得出函数调用后形参的类型,然后将该类型与函数参数列表中形参的类型进行对比、匹配,从而得出T的类型。
如果发现不能匹配,则再次运用叠加规则”推导“出T的类型(我们将在验证规则3时遇到这种情况)。
以图1中的情况为例:
T& w_a (形参列表中的)
int& w_a (函数调用后形参的实际类型,由叠加规则决定)
对比知,T为int型。
2.验证规则2
图说:
图2
这似乎已经验证了规则2,但请看下图:
图3
不知是否有人会惊讶,a明明是右值引用,为什么会调用void f(int& lfa)?换句话说,a什么时候变成了左值?
现在,要告诉大家一个结论(相信许多人都知道,就当在下是重复吧):
C++标准规定,具名的右值引用被当作左值。[注 6]这一规定的意义在于,右值引用本来是用于实现移动语义,因而需要绑定一个对象的内存地址,然后具有修改这一对象内容的权限,这些操作与左值绑定完全一样。右值绑定与左值绑定的分野在于确定函数重载时的分辨。对于移动构造成员函数与移动赋值运算符成员函数,其形、实参数结合时是按照右值引用处理;而在这两个成员函数体内部,由于形参都是具名的,因而都被当作左值,这就可以用该形参来修改传入对象的内部状态。另外,右值引用作为xvalue(临终值)本来是用于移动语义中一次性搬空其内容。具名使其具有更为持久的生存期,这是危险的,因而规定具名后为左值引用,除非程序显式指定其类型强制转换为右值引用。
——维基百科 地址:http://zh.wikipedia.org/wiki/%E5%8F%B3%E5%80%BC%E5%BC%95%E7%94%A8
另外,从上图也可以看出,&&和&的不同可以作为重载标志。
现在,相信大家也不再惊讶。回过头来看图2,我们明白,这个验证是无效的,ra被当成左值,相当于还是在验证规则1。那么,怎么办呢?看下图:
图4
虽然结论没有变化,但这种验证方法是有效的。
读者可以在图4代码的基础上,加入图3中的两个f函数,然后在main函数中写f(rt());会得到“右值:1”这样的输出。为缩短文章篇幅,这里就不截图了,请读者自己验证。
关于图4的代码,说以下几点:
1.前面说过,具名右值引用按左值引用处理,所以,要达到实验目的,不能将具名右值引用传给函数wai(),所以我们传函数返回值这样的不具名右值引用。
2.如果我们返回局部变量或是临时对象的引用(比如在rt()函数中写int a=1;return a++;,哪怕将int a=1;放在全局,也是不行的,因为a++就是返回++前a的一份拷贝,属于临时对象),结果是不正确的(得不到输出1)。(具体原因在下暂时还不清楚,可能是后边的代码执行时将临时变量的空间覆盖(重写)了,在下反汇编单步也没找出确切的答案(在下汇编学得不怎么样),这里烦请有知道原因的大牛给出指点,在下感激不尽,先行谢过)
3.就像大家在图4中看到的那样,rt()函数中必须将全局变量a强制类型转换为int&&型再返回,否则,如果写成return a;,编译器将产生类似“无法将右值引用绑定到左值”的报错,原因是具名右值引用a被当做左值。
4.void wai(const T& w_a)中的const不能省,原因是非常量引用(T&)不能接受右值引用。
5.void nei(const int& n_a)中的const也不能省,正如大家在图4中看到的,在wai()中执行nei(w_a);时,w_a为const int&类型。
简单说一下T的推导:
const T& w_a (参数列表中)
const int& w_a (函数调用后w_a的实际类型)
对比知,T为int型。
至此,我们可以确定,表1中红色的A是正确的,A&的说法有误。
3.验证规则3
图说:
这里只说一下T的推导。如下:
T&& w_a (参数列表中w_a的类型)
int& w_a (函数调用后w_a的实际类型)
显然,此时无法直接匹配。这里我们运用表2(之所以用表2,是因为表2比表1更加直观)中的第2条A& + && -> A&,推出T为int&类型。
4.验证规则4
图说:
这里首先说一点,前边我们说过,非常量左值引用不能接受右值引用,上图中,void nei(int& n_a),w_a为int&&类型,那么,rt()中的nei(w_a);是如何通过的呢?
不要忘了,虽然w_a显示为int&&类型,但它是具名右值引用,所以作为左值引用处理,自然能够通过。如果我们将void nei(int& n_a)改为void nei(int&& n_a),反而不能通过(w_a被当做int&型,int&&不能接受int&),读者可以自己试一试。
再说一下T的推导:
T&& w_a (参数列表中w_a的类型)
int&& w_a (函数调用后w_a的实际类型,不考虑C++11将其视为int&)
对比,知T为int型。
至此,4个引用叠加规则和相应的模板参数类型推导都说完了,谢谢大家!
后记:
在下爱钻研,喜探究,实事求是;但另一方面,又着实才疏学浅,能力有限,所以只能做一些基础性的工作。但即便如此,也难免有疏漏乃至错误之处,这里,在
下恳请大家批评指正,不吝赐教。您的批评指正就是在下不断进步的源泉!