您好,登錄后才能下訂單哦!
背景:
最近在學習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個引用疊加規則和相應的模板參數類型推導都說完了,謝謝大家!
后記:
在下愛鉆研,喜探究,實事求是;但另一方面,又著實才疏學淺,能力有限,所以只能做一些基礎性的工作。但即便如此,也難免有疏漏乃至錯誤之處,這里,在
下懇請大家批評指正,不吝賜教。您的批評指正就是在下不斷進步的源泉!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。