1. 引言
在 C++ 98 初推出的時候,C++ 並沒有 Reference of Reference 的概念 (&&),所以,開發者不但不能直接使用 &&,即使用了 typedef 將 reference type 包裝好,然後再加上 &,編譯器都一律會報錯。
後來,C++11 終於開放了對 Reference of Reference (&&) 的支援。不過由於 C++11 也同時引進了 Move Semantics (也是 &&) 的寫法,所以,Reference of Reference 就會跟 Move Semantics 有所衝突。
以下是一個例子,開發者想做的是:func(k) 要跑的,是 lvalue 的版本的 func()(第一個,單 & 的版本),而非 rvalue 的版本( &&,也就是 move semantics 的版本):
#include <iostream>
typedef int& INTR;
template<class T>
void func(T& i) {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
template<class T>
void func(T&& i) {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
int main() {
int i = 123;
INTR j = i; // int& j = i
INTR& k = j; // int&& k = j
func(k); // k is lvalue (should run T(&i))
func(123); // 123 is rvalue (should run T(&&i))
return 0;
}
它的輸出應為:
void func(T&) [with T = int]
void func(T&&) [with T = int]
這裏看起來,明明 k就應該要是 int &&,但編譯器居然知道它不是指 rvalue,而是指 reference of reference。當中的原因是, C++ 11 新增了 Reference Collapsing (引用折叠) 的規則,確保編譯器在在大部分情況下(也就是使用了 typedef / decltype / template 的情況下), 都能辨認出 Reference of Reference 仍然是 "reference",而非 "rvalue" 。
---
2. Reference Collapsing 四個規則
引用折叠有四個主要的編譯規則(其實有更多,不過不太重要,在此不述):
A& &
becomesA&
A& &&
becomesA&
A&& &
becomesA&
A&& &&
becomesA&&
反正重點是,編譯器會在碰到兩次 && 的時候,它才會當它是指 rvalue。其他情況的話,它會把 Reference of Reference (&&) 都降級成 Reference (&)。
---
3. Universal Reference / Forwarding Reference (轉發引用)
既然 && 會在編譯的時候,自動按情況降級成 &,那麼我們在寫 function template 的時候,是否可以寫少一個,然後讓編譯器自行將 function 變成兩個 (&& 和 &) 的版本呢?答案是可以的。
先看以下例子:
#include <iostream>
template<class T>
void function(T&& t) {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
int main() {
int i = 123;
function(i);
function(996);
function(std::move(i));
return 0;
}
它的執行結果為:
void function(T&&) [with T = int&]
void function(T&&) [with T = int]
void function(T&&) [with T = int]
首先,i 是 lvalue (int &),所以 T = int& = T&&& = T & ,
其餘的 996 和 std::move(i) 則因為本身是 rvalue(int &&),所以直接 T = int。
---
4. Perfect Forwarding 完美轉發
有一個情況要注意:如果在 template function 中,我們想要把變數傳到下一個 function 的話, 正常來講,我們可能會這樣做:
template<class T>
void function(T&& t) {
subFunction(t);
}
然後,我們會期待傳入 subFunction(t) 的 t 應該是 T&& / rvalue,但其實並不一定 ...
以下有一個例子:
#include <iostream>
void subFunction(int&& i) {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
void subFunction(int& i) {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
template<class T>
void function(T&& t) {
std::cout << "Value: " << t << std::endl;
std::cout << __PRETTY_FUNCTION__ << std::endl;
subFunction(t);
subFunction(std::forward<T>(t));
subFunction(static_cast<T&&>(t));
subFunction((T&&)(t));
}
int main() {
int i = 123;
function(i);
function(996);
function(std::move(i));
return 0;
}
它的輸出為:
Value: 123
void function(T&&) [with T = int&]
void subFunction(int&)
void subFunction(int&)
void subFunction(int&)
void subFunction(int&)
Value: 996
void function(T&&) [with T = int]
void subFunction(int&)
void subFunction(int&&)
void subFunction(int&&)
void subFunction(int&&)
Value: 123
void function(T&&) [with T = int]
void subFunction(int&)
void subFunction(int&&)
void subFunction(int&&)
void subFunction(int&&)
由結果可見,如果只用 subFunction(t),編譯器會認為 t 是一個 lvalue。
在 function 的 scope 之中,t 的確是一個變數,是 T&& 的 reference (T&& & = T&)。
如果要解決這個問題,把 && 正確地傳到 subFunction,最簡單的方法,就是把 t 的型態轉強制換成 T&&。如此,傳入來的 t 就能完全不經處理地傳到 subFunction。在解 subFunction 的時候,編譯器會找回最初的 i / 996 / std::move(i),再決定執行那一個版本的 subFunction。
而 std::forward<T>,其實就是 static_cast<T&&> 的語法糖而已。
---
小結
C++11 為了讓 Reference of Reference 變得簡單,並加上對 Move Semantics 的支援,同時令編譯器做到最大程度的向後兼容(不加新 keyword + 舊程式碼可跑),似乎把事情變得更複雜了:它使用了 Reference Collapsing 將不同情況的 && 分開。但同時,這種做法也導致 template function 如果用了 &&,很大機會要在 function call 時,對 variables 加上 static_cast,才能正常地運作 。
但是,我們只要無腦採用 universal reference (兩個 &&)加上 std::forward<T>,基本上就能解決 99% 的問題。向好的方向去想:使用 move semantics 加上 universal reference,不但能加快執行時間,或許更能節省程式碼行數,加快 debug 效率。
No comments:
Post a Comment