這篇文章主要是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 2 3 4 5 6 7 8 9 string one ("one" ) ;const string two ("two" ) ;string three () { return "three" ; }const string four () { return "four" ; }one; two; three(); four();
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 2 3 4 void print (const string &) { cout << "print(const string&)" << endl ; }void print (string &&) { cout << "print(string&&)" << endl ; }void RvalueTest (string && str) { print(str); }RvalueTest(string ());
在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 2 3 4 5 template <typename T>T&& Move (.... value) { return static_cast <T&&>(value); }
這邊先來決定如何傳遞參數,Call by value 第一個被否決,接著就是T&
跟T&&
的選擇,根據前面的Reference Collapsing Rules,如果是T&
的話一律會摺疊成T&
,而T&無法繫結至modifiable rvalue,而如果是T&&
的話,不管Lvalue跟Rvalue都可以順利繫結。
接著我們來測試第一版的程式碼
1 2 3 4 quark(Move(up)); quark(Move(down)); quark(Move(strange())); quark(Move(charm()));
印出的結果是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 t: up T: string & T&&: string & t: down T: const string & T&&: const string & t: strange() T: string T&&: string && t: charm() T: const string T&&: const string &&
顯然結果錯了,原因在於實際參數是Lvalue的話,T會被推導成U&,而T&&的結果依然是U&,變成Move傳回去的語意是個Lvalue,因此導致上面的結果。 所以我們要做的,就是把U&或U&&一律轉成U&&,也就是std::remove_refernce存在的理由。改寫我們的程式,先用RemoveRefenece取得Primitive Type, 然後加上&&之後就可以得到正確的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 template <typename T>struct RemoveReference { typedef T type; }; template <typename T>struct RemoveReference <T&> { typedef T type; }; template <typename T>struct RemoveReference <T&&> { typedef T type; }; template <typename T>typename RemoveReference<T>::type&& Move (T&& t) { return static_cast <RemoveReference<T>::type&&>(t); }
gcc的實作就類似於這樣。 接著來討論forward該怎麼做,從上面我們可以知道,我們只能用T&&來傳遞參數。 先來一組helper function來驗證程式的正確性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void inner (std ::string &str) { cout << "inner(std::string &str)" << endl ; } void inner (const std ::string &str) { cout << "inner(const std::string &str)" << endl ; } void inner (const std ::string && str) { cout << "inner(const std::string&& str)" << endl ; } void inner (std ::string && str) { cout << "inner(std::string&& str)" << endl ; } template <typename T> void outer (string && value) { return inner(Forward(value)); } outer(up); outer(down); outer(strange()); outer(charm());
而我們第一版的Forward的實作
1 2 3 4 5 template <typename T>T&& Forward (T& value) { return static_cast <T&&>(value); }
輸出結果則是
1 2 3 4 inner(std ::string && str) inner(const std ::string && str) inner(std ::string && str) inner(const std ::string && str)
從上面知道,value是個左值,所以Type是U&,T被推導成U,T&&被強制轉換成右值,所以輸出的結果如上。避免的方法就是強迫加上template參數。
1 2 3 4 5 template <typename T> void outer (string && value) { return inner(Forward<T>(value)); }
重新執行程式,這下結果符合我們的需求了
1 2 3 4 inner(std ::string &str) inner(const std ::string &str) inner(std ::string && str) inner(const std ::string && str)
這邊還有幾點要說明的 之前的範例,value是左值,而T可能是U&或是U,T&的結果是U&,可以繫結住左值沒有問題。萬一Forward的參數是個右值怎麼辦?
1 inner(Forward<T>(string ()));
我們需要另外一個function,解決function resolution的問題。
1 2 3 4 5 template <typename T>T&& Forward (T&& value) { return static_cast <T&&>(value); }
在傳進來的是個右值時,T被推導成U。T&&正好可以綁定一個Rvalue,解決上面的問題。不過問題又來了,如果是左值的話,T是U&,T&是U&,T&&還是U&,變成兩個function擁有兩個一模一樣的參數型態,Compiler不知道該選哪個。 解決方案就是套用上面Move所引進的RemoveReference,還原成Primitive Type。
1 2 3 4 5 6 7 8 9 10 template <typename T>T&& Forward (typename RemoveReference<T>::type& value) { return static_cast <T&&>(value); } template <typename T>T&& Forward (typename RemoveReference<T>::type&& value) { return static_cast <T&&>(value); }
這樣子有另外一個好處,這個方案禁止了型別推導,不會再有Forward(value)
的存在,編譯時期就能指出錯誤。