這篇文章主要是Rvalue References: C++0x Features in VC10, Part 2的閱讀筆記,在原文撰寫過程當中,規格有所變動,因此根據最新的規則做補充說明。
Copy Problems
C++98/03的時候,最令人詬病的問題,就是建立太多臨時物件,Value Semantics的意思就是複製出來的物件跟原先是獨立的,不會互相干擾
Lvalue and Rvalue
在C++98/03時期,有這麼一條規則Every C++ expression is either an lvalue or an rvalue.
,Lvalue是在運算過後留下來的續存物件,而Rvalue是運算過後生命期就結束的臨時物件。
殂此之外,C++98/03裡還有一條規則A function call is an lvalue if and only if the result type is a reference
1 | string one("one"); |
- Type&可以繫結到一個modifiable lvalue,而如果要繫結到modifiable rvalue,C++規定禁止(Visual C++除外,可以把警告層級從3調到4,會警告你這樣很危險,而gcc跟clang則是輸出錯誤訊息。)
- const Type&可以繫結到任何一種型態,不過不能對Rvalue做任何修改,因此不能對即將消滅的臨時物件採取任何行動
而在C++11之後,引進了Rvalue reference
,解決了這個問題。
- Type&&可以繫結到一個modifiable rvalue,而不能繫結到modifiable lvalue,需要強制轉型。
- const Type&&可以繫結任何形態
每個reference都有一個名字,所以Bind到Rvalue的refernce,他是一個Lvalue,因此以下的程式碼。
1 | void print(const string&) { cout << "print(const string&)" << endl; } |
在RValueTest執行結束之前,str是個合法的物件,因此被當作Lvalue,會執行第一個print。
為了學習Rvalue的觀念,自行打造move跟forward函數。
先看C++11之後引進的Reference Collapsing Rules
- T& + & => T&
- T&& + & => T&
- T& + && => T&
- T&& + T&& => T&&
而Move的用途就是明確指出不管物件是Lvalue或Rvalue,一律轉成Rvalue就是了。
而Forward的用途把外面的參數跟語意原封不動的傳進去內部,是Lvalue就是Lvalue,而Rvalue就是Rvalue。
先從比較簡單的Move開始看起,我門打造的第一版Move大概像這樣。
由於Move是個template function,必須進行Template Argument Deduction,此時引進了一條新的規則。
如果傳進來的是Lvalue的話,將會推導成T&,反之如果是Rvalue的話,就會推導成T(根據Reference Collapsing Rules,T和T&&都符合要求,為了解決歧異性,這邊強制要求推導成T)
1 | template <typename T> |
這邊先來決定如何傳遞參數,Call by value第一個被否決,接著就是T&
跟T&&
的選擇,根據前面的Reference Collapsing Rules,如果是T&
的話一律會摺疊成T&
,而T&無法繫結至modifiable rvalue,而如果是T&&
的話,不管Lvalue跟Rvalue都可以順利繫結。
接著我們來測試第一版的程式碼
1 | quark(Move(up)); |
印出的結果是
1 | t: up |
顯然結果錯了,原因在於實際參數是Lvalue的話,T會被推導成U&,而T&&的結果依然是U&,變成Move傳回去的語意是個Lvalue,因此導致上面的結果。
所以我們要做的,就是把U&或U&&一律轉成U&&,也就是std::remove_refernce存在的理由。改寫我們的程式,先用RemoveRefenece取得Primitive Type, 然後加上&&之後就可以得到正確的值。
1 | template <typename T> |
gcc的實作就類似於這樣。
接著來討論forward該怎麼做,從上面我們可以知道,我們只能用T&&來傳遞參數。
先來一組helper function來驗證程式的正確性。
1 | void inner(std::string &str) |
而我們第一版的Forward的實作
1 | template <typename T> |
輸出結果則是
1 | inner(std::string&& str) |
從上面知道,value是個左值,所以Type是U&,T被推導成U,T&&被強制轉換成右值,所以輸出的結果如上。避免的方法就是強迫加上template參數。
1 | template <typename T> |
重新執行程式,這下結果符合我們的需求了
1 | inner(std::string &str) |
這邊還有幾點要說明的
之前的範例,value是左值,而T可能是U&或是U,T&的結果是U&,可以繫結住左值沒有問題。萬一Forward的參數是個右值怎麼辦?
1 | inner(Forward<T>(string())); |
我們需要另外一個function,解決function resolution的問題。
1 | template <typename T> |
在傳進來的是個右值時,T被推導成U。T&&正好可以綁定一個Rvalue,解決上面的問題。不過問題又來了,如果是左值的話,T是U&,T&是U&,T&&還是U&,變成兩個function擁有兩個一模一樣的參數型態,Compiler不知道該選哪個。
解決方案就是套用上面Move所引進的RemoveReference,還原成Primitive Type。
1 | template <typename T> |
這樣子有另外一個好處,這個方案禁止了型別推導,不會再有Forward(value)
的存在,編譯時期就能指出錯誤。