1. 程式人生 > >《C++0x漫談》系列之:右值引用(或“move語意與完美轉發”)(下)

《C++0x漫談》系列之:右值引用(或“move語意與完美轉發”)(下)

C++0x漫談》系列之:右值引用

或“move語意與完美轉發”(下)

By 劉未鵬(pongba)

C++0x漫談》系列導言

這個系列其實早就想寫了,斷斷續續關注C++0x也大約有兩年餘了,其間看著各個重要proposals一路review過來:rvalue-referencesconceptsmemory-modelvariadic-templatestemplate-aliasesauto/decltypeGCinitializer-lists…

總的來說C++09C++98相比的變化是極其重大的。這個變化體現在三個方面,一個是形式上的變化,即在編碼形式層面的支援,也就是對應我們所謂的程式設計正規化

(paradigm)C++09不會引入新的程式設計正規化,但在對泛型程式設計(GP)這個正規化的支援上會得到質的提高:conceptsvariadic-templatesauto/decltypetemplate-aliasesinitializer-lists皆屬於這類特性。另一個是內在的變化,即並非程式碼組織表達方面的,memory-modelGC屬於這一類。最後一個是既有形式又有內在的,r-value references屬於這類。

這個系列如果能夠寫下去,會陸續將C++09的新特性介紹出來。鑑於已經有許多牛人寫了很多很好的tutor這裡這裡,還有C++標準主頁上的一些introductive

proposals,如這裡,此外C++社群中老當益壯的Lawrence Crowl也在google做了)。所以我就不作重複勞動了:),我會盡量從一個巨集觀的層面,如特性引入的動機,特性引入過程中經歷的修改,特性本身的最具代表性的使用場景,特性對程式設計正規化的影響等方面進行介紹。至於細節,大家可以見每篇介紹末尾的延伸閱讀。

右值引用導言

右值引用(及其支援的Move語意和完美轉發)是C++0x將要加入的最重大語言特性之一,這點從該特性的提案在列表上高居榜首也可以看得出來。從實踐角度講,它能夠完美解決C++中長久以來為人所詬病的臨時物件效率問題。從語言本身講,它健全了C++中的引用型別在左值右值方面的缺陷。從庫設計者的角度講,它給庫設計者又帶來了一把利器。從庫使用者的角度講,不動一兵一卒便可以獲得“免費的”效率提升

完美轉發

完美轉發問題——不完美解決方案——模板引數推導規則——完美轉發

動機

關於“完美轉發”這個特性,其實已經講得非常清楚了,諸位可以直接去看N1385,如果實在還是覺得迷糊就再回來聽我嘮叨吧:-)

在泛型編碼中經常出現的一個問題是(這個問題在實際中出現的場景很多,我們留到文章末尾再提,目前我們將這個特定的問題先提取孤立出來考慮):

如何將一組引數原封不動地轉發給另一個函式

注意,這裡所謂“原封不動”就是指,如果引數是左值,那麼轉發給的那個函式也要接受到一個左值,如果引數是右值,那麼後者要接受到一個右值;同理,如果引數是const的,那麼轉發給的那個函式也要接受到一個const的值,如果是non-const的,那麼後者也要接受到一個non-const的值。

總之一句話:

保持引數的左值/右值、const/non-const屬性不變

聽上去很簡單嗎?不妨試試看。

(不完美的)解決方案

假設我們要寫一個泛型轉發函式ff要將它的引數原封不動地轉發給g(不管g的引數型別是什麼):

template<typename T>

void f(/*what goes here?*/ t)

{

g(t);

}

上面的程式碼中,f的引數t的型別是什麼?TT&const T&

我們一個個來分析。

Value

如果t的型別是T,即:

// take #1

template<typename T>

void f(T t)

{

g(t);

}

那麼很顯然,不能滿足如下情況:

void g(int& i) { ++i; }

int myInt = 0;

f(myInt); // error, the value g() has incremented is a local value(a.k.a. f’s argument ‘t’)

即,不能將左值轉發為左值。

Const&

如果t的型別為const T&,即:

// take #2

template<typename T>

void f(const T& t)

{

g(t);

}

則剛才的情況還是不能滿足。因為g接受的引數型別為non-const引用。

Non-const&

那如果t的型別是T&呢?

// take #3

template<typename T>

void f(T& t)

{

g(t);

}

我們驚訝地發現,這時,如果引數是左值,那麼不管是const左值還是non-const左值,f都能正確轉發,因為對於const左值,T將會被推導為const UU為引數的實際型別)。並且,對於const右值,f也能正確轉發(因為const引用能夠繫結到右值)。只有對non-const右值不能完美轉發(因為這時T&會被推導為non-const引用,而後者不能繫結到右值)。

即四種情況裡面有三種滿足了,只有以下這種情況失敗:

void g(const int& i);

int source();

f(source()); // error

如果f是完美轉發的話,那麼f(source())應該完全等價於g(source()),後者應該通過編譯,因為g是用const引用來接受引數的,後者在面對一個臨時的int變數的時候應該完全能夠繫結。

而實際上以上程式碼卻會編譯失敗,因為f的引數是T&,當面對一個non-constint型右值(source()的返回值)時,會被推導為int&,而non-const引用不能繫結到右值。

好,現在的問題就變成,如何使得non-const右值也被正確轉發,用T&f的引數型別是行不通的,唯一能夠正確轉發non-const右值的辦法是用const T&來接受它,但前面又說過,用const T&行不通,因為const T&不能正確轉發non-const左值。

Const& + non-const&

那兩個加起來如何?

template<typename T>

void f(T& t)

{

g(t);

}

template<typename T>

void f(const T& t)

{

g(t);

}

一次過載。我們來分析一下。

對於non-const左值,過載決議會選中T&,因為繫結到non-const引用顯然優於繫結到const引用(const T&)。

對於const左值,過載決議會選中const T&,因為顯然這是個更specialized的版本。

對於non-const右值,T&根本就行不通,因此顯然選中const T&

對於const右值,選中const T&,原因同第二種情況。

可見,這種方案完全保留了引數的左右值和const/non-const屬性。

值得注意的是,對於右值來說,由於右值只能繫結到const引用,所以雖然const T&並非“(non-)const右值”的實際型別,但由於C++03只能用const T&來表達對右值的引用,所以這種情況仍然是完美轉發。

組合爆炸

你可能會覺得上面的這個方案(const& + non-const&)已經是完美解決方案了。沒錯,對於單參的函式來說,這的確是完美方案了。

但是如果要轉發兩個或兩個以上的引數呢?

對於每個引數,都有const T&T&這兩種情況,為了能夠正確轉發所有組合,必須要2N次方個過載

比如兩個引數的:

template<typename T1, typename T2>

void f(T1& t1, T2& t2) { g(t1, t2); }

template<typename T1, typename T2>

void f(const T1& t1, T2& t2) { g(t1, t2); }

template<typename T1, typename T2>

void f(T1& t1, const T2& t2) { g(t1, t2); }

template<typename T1, typename T2>

void f(const T1& t1, const T2& t2) { g(t1, t2); }

(完美的)解決方案

理想情況下,我們想要:

template<typename T1, typename T2, … >

void f(/*what goes here?*/ t1, /**/ t2, … )

{

g(t1, t2);

}

填空處應該填入一些東西,使得當t1對應的實參是non-const/const的左/右值時,t1的型別也得是non-const/const的左/右值。目前的C++03中,non-const/const屬性已經能夠被正確推匯出來(通過模板引數推導),但左右值屬性還不能。

明確地說,其實問題只有一個:

對於non-const右值來說,模板引數推導機制不能正確地根據其右值屬性確定T&的型別(也就是說,T&會被編譯器不知好歹地推導為左值引用)。

修改T&non-const右值的推導規則是可行的,比如對這種情況:

template<typename T>

void f(T& t);

f(1);

規定T&推導為const int&

但這顯然會破壞既有程式碼。

很巧的是,右值引用能夠拯救世界,右值引用的好處就是,它是一種新的引用型別,所以對於它的規則可以任意制定而不會損害既有程式碼,設想:

template<typename T >

void f(T&& t){ g(t); }

我們規定:

如果實參型別為右值,那麼T&&就被推導為右值引用。

如果實參型別為左值,那麼T&&就被推導為左值引用。

Bingo!問題解決!為什麼?請允許我解釋。

f(1); // T&& 被推導為 int&&,沒問題,右值引用繫結到右值。

f(i); // T&& 被推導為 int&,沒問題,通過左值引用完美轉發左值。

等等,真沒問題嗎?對於f(1)的情況,t的型別雖然為int&&(右值引用),但那是否就意味著t本身是右值呢?既然t已經是具名(named)變量了,因此t就有被多次move(關於move語意參考上一篇文章)的危險,如:

void dangerous(C&& c)

{

C c1(c); // would move c to c1 should we allow treating c as a rvalue

c.f(); // disaster

}

在以上程式碼中,如果c這個具名變數被當成右值的話,就有可能先被move掉,然後又被悄無聲息的非法使用(比如再move一次),編譯器可不會提醒你。這個邪惡的漏洞是因為c是有名字的,因此可以被多次使用。

解決方案是把具名的右值引用作為左值看待

但這就使我們剛才的如意算盤落空了,既然具名的右值引用是左值的話,那麼f(1)就不能保持1的右值屬性進行轉發了,因為f的形參t的型別(T&&)雖然被推導為右值引用(int&&),但t卻是一個左值表示式,也就是說f(1)把一個右值轉發成了左值。

最終方案

通過嚴格修訂對於T&&的模板引數推導原則,以上問題可以解決。

修訂後的模板引數推導規則為:

如果實參是左值,那麼T就被推導為U&(其中U為實參的型別),於是T&& = U& &&,而U& &&則退化為U&(理解為:左值引用的右值引用仍然是左值引用)。

如果實參是右值,那麼T就被推導為U,於是T&& = U&&(右值引用)。

如此一來就可以這樣解決問題:

template<typename T>

void f(T&& t)

{

g(static_cast<T&&>(t));

}

想想看,如果實參為左值,那麼T被推導為U&T&&U& &&,也就是U&,於是static_cast<T&&>也就是static_cast<U&>,轉發為左值。

如果實參為右值,那麼T被推導為UT&&U&&static_cast<T&&>也就是static_cast<U&&>,不像t這個具名的右值引用被看作左值那樣,static_cast<U&&>(t)這個表示式由於產生了一個新的無名(unnamed)值,因而是被看作右值的。於是右值被轉發為了右值。

擴充套件閱讀

下篇預告

下篇會寫variadic templates。然後介紹tr1::tuple的新版實現。