左值與右值程序員
左值與右值的概念要追溯到 C 語言,由 C++ 語言繼承了上來。C++ 03 3.10/1 如是說:「Every expression is either an lvalue or an rvalue.」左值與右值是指表達式的屬性,而非對像的屬性。express
左值具名,對應指定內存域,可訪問;右值不具名,不對應內存域,不可訪問。臨時對像是右值。左值可處於等號左邊,右值只能放在等號右邊。區分表達式的左右值屬性有一個簡便方法:若可對錶達式用 & 符取址,則爲左值,不然爲右值。函數
注意區分 ++x 與 x++。前者是左值表達式,後者是右值表達式。前者修改自身值,並返回自身;後者先建立一個臨時對像,並用 x 的值賦之,後將修改 x 的值,最後返回臨時對像。測試
函數的返回值通常狀況下是右值,C++ 03 5.2.2/10 如是說:「A function call is an lvalue if and only if the result type is a reference.」好比有 vector<int> v 對像,則 v[0] 即爲左值,由於 vector 容器的 [] 算符重載函數的返回值爲引用。ui
左值與右值都可以聲明爲 const 和 non-const。.net
拼接字串的問題blog
上面提到函數返回值通常爲右值,也即臨時對像。對於內置類型(built-in type)來講,臨時對像仍是可忍的。但對於容器對像來講就是極大的浪費了。舉一個 C++ 98/03 標準下最通俗的例子,拼接字符串:繼承
圖一內存
圖一第 18 行,短短一句,背後動做極其複雜。我要把 string 對像和常量字串交替拼接起來,問題重點在於如何重載 + 算符。有以下幾點須要考慮:作用域
- 過程當中分別出現 string 對像與常量字串的加法、string 對像與 string 對像的加法。所以須要重載多種 + 算符函數。(加法自左至右)
- 好比 string 對像與常量字串的加法,返回的將是一個新生成的 string 對像,所以必須返回這個對像的複本,是臨時對像,是右值。又因爲加法是連續運算的,下一個加法的重載函數爲了接收這一右值,參數表只得寫成傳值的形式,也即將此臨時對像再複製一次,纔可傳到函數體內操做。總的來講,就是臨時對像由前一個函數體轉到另外一個函數體,須要深度複製兩次。
- 由第二點可知,僅僅由一個加號過渡到另外一個加號,就要產生兩個曇花一現、轉瞬即逝的臨時對像複本。若每一個字串都很長,對像都很大,拼接個數又特別多,這要產生多少垃圾?爲什麼不能把前一個函數返回時產生的臨時對像不用複製,直接拿給下一個函數用呢?也就是從前一個函數「移動」到後一個函數體中。
- 對於第三點,C++ 98/03 不容許這麼作。由於語義上不支持。因爲缺少「移動語義」,前一個函數產生的臨時對像將在函數體退出時析構,外部要想得到只能使用其複本,本體已經不存在了。
右值引用和移動語義
針對上述拼接字串的問題,若說,函數返回時產生的臨時對像須要複製出去還情有可原——畢竟人家的做用域到頭兒了,本體的確不能傳遞到外部,只能由複本代勞(這是 C++ 與 C# 最大的不一樣之一);不過話又說回來,複本都複製出來了,爲什麼傳遞到下一個函數體內還須要再複製一次呢?C++ 98/03 說得是義正詞嚴:
「由於我規定了,右值不但不能取址,連引用都不能取!誰讓丫傳的是臨時對像,是右值,傳參只能傳值!」
話說得多氣人吶!憑什麼連引用都不能取?傳值就意味着深度複製。C++ 標準委員會發現了這一問題,決定在 C++ 0x 新標準中補充「右值引用」和「移動語義」。
移動語義:將對方掏空,實體吸取給我本身。見《測試 VS 2010 對 C++ 0x 標準的謹慎支持》。
舉一個臨時對像由一個函數傳往另外一個函數的例子以說明問題。由例子可見,Sck 函數使用右值引用重載版本,接收 Fck 函數返回的臨時對像。而在 Fck 函數返回時,完成了一次 Sb 對像的複製。如圖:
圖二
關於右值引用和移動語義的更多例子,請參見微軟 VC 官方博客:《Rvalue Reference》。
右值引用重載函數幾點
- 移動構造重載函數和移動賦值算符(assignment operators:=、^=、+=,etc.)重載函數毫不會隱式聲明,必須本身定義。
- 默認構造函數會被用戶本身顯式定義的構造函數壓制,包括用戶自定義複製構造函數和移動構造函數。因故若用戶已自定義複製和移動構造函數,且須要無參構造函數時,也須要本身定義。
- 隱式複製構造函數會被用戶本身顯式定義的複製構造函數覆蓋,而不是自定義的移動構造函數。
- 隱式複製賦值重載函數會被用戶本身顯式定義的複製賦值重載函數覆蓋,而不是自定義的移動賦值重載函數。
總之一句話,一個類定義完了,程序員嘛也無論,默認構造函數、默認複製構造函數、默認複製賦值函數,編譯器都會自動生成。而移動語義的構造函數和賦值函數,則必須由程序員本身顯式定義方可以使用。
操做右值對像實現移動語義
操做右值對像實現移動語義,須使用 std::move () 方法。不管是對類對像,仍是對類對像的成員變量。使用 move 方法須要引用 <utility> 頭文件。詳見下例:
圖三
外圍函數向內部函數準確傳參的問題
見以下代碼塊:
void Outer ( params ) { Inner ( params ); }
由 Outer 函數接收參數後,要準確無誤地傳遞給 Inner 函數。所謂的準確無誤包括 params 的左、右值屬性和 const / non-const 屬性。此也即「參數傳導語義」。實現這一語義的目的是 Inner 函數的類型檢查信息能夠與 Outer 外部互通,所以由 Outer 到 Inner 之間的參數傳導不能對參數屬性有任何的改變。
在 C++ 98/03 標準下,咱們可使用左值引用標識參數類型: T& params;但若我往裏傳常量呢?常量是右值,傳不進去。好,那改爲 const T& params 好了,這下左右值均可以傳了;但若我要在函數體內修改 params 的值呢?……
《C++ 0x 之 Lambda:賢妻與嬌娃,你娶誰當老婆?聽 FP 如何點化 C++》裏說:C++ 是萬能的。別覺得 C++ 沒轍了,我能夠重載啊!一種版本我知足不了你的全部要求,我重載出知足你要求的全部版本的函數就行了唄!
嗯……C++ 果然賢惠!好,我一個參數表有 64 個參數,你把全部版本都重載去吧!估計得有 2^64 個這麼多……
傳導模板:forward<>
話說 C++ 0x 以前的 C++ 在這方面表現得實在是太糙了,簡直無法兒看……咱們所期待的完美解決方案是隻用一個模板便可處理全部狀況,而重載函數再能用也不能這麼用。好在 C++ 0x 的 <utility> 頭文件中有 forward 模板:
template < typename T > void Outer ( T&& t )
{
Inner ( std::forward<T> ( t ) );
}
不錯,寫這麼一個就解決了。首先 Outer 函數參數表使用 T&& 類型接收參數。推導過程以下:
一句話,T&& 模板類型能夠保留參數信息。
Outer 使用 T&& 是解釋清楚了,那 forward<> 是如何保證由 Outer 到 Inner 的平穩過渡呢?若要知 std::move () 和 std::forward () 是如何實現的,請參見:《C++ 0x 之移動語義和傳導模板如何實現》。