1. 程式人生 > >C++11 圖說VS2013下的引用疊加規則和模板參數類型推導規則

C++11 圖說VS2013下的引用疊加規則和模板參數類型推導規則

反匯編 cto 構造 這不 gif 怎麽辦 由於 pla 覆蓋

背景:
最近在學習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個引用疊加規則和相應的模板參數類型推導都說完了,謝謝大家!

後記:

在下愛鉆研,喜探究,實事求是;但另一方面,又著實才疏學淺,能力有限,所以只能做一些基礎性的工作。但即便如此,也難免有疏漏乃至錯誤之處,這裏,在

下懇請大家批評指正,不吝賜教。您的批評指正就是在下不斷進步的源泉!

C++11 圖說VS2013下的引用疊加規則和模板參數類型推導規則