【1】為什么引入完美轉發?
在函數模板編程中,常有一種場景是把模板參數轉發給另一個調用函數,這時候如果只提供值傳遞版本會顯得效率太低。看以下代碼:
1 template<class TYPE, class ARG> 2 TYPE* getInstance(ARG arg) 3 { 4 TYPE* pRet = nullptr; 5 pRet = new TYPE(arg); 6 return pRet; 7 }
代碼很簡單,就是用ARG參數去初始化一個TYPE類型的對象,然后返回該對象指針。
考慮一下,如果ARG類型是一個自定義類型,那么這樣的值傳遞會是比較大的性能開銷。
有沒有辦法改進一下?再看以下代碼:
1 template<class TYPE, class ARG> 2 TYPE* getInstance(const ARG& arg) 3 { 4 TYPE* pRet = nullptr; 5 pRet = new TYPE(arg); 6 return pRet; 7 }
這段代碼將傳入參數類型改為了“萬能”的常量左值引用,可以接受任何類型,可以解決性能開銷的問題。
但是,但是,還不夠靈活,假如我們想TYPE接受一個右值去初始化呢?
那么有沒有可以把參數連同類型一起轉發的方案呢?當然有,沒有C++辦不到的。
C++11就提供了這樣能力既完美轉發。代碼如下:
1 template<class TYPE, class ARG> 2 TYPE* getInstance(ARG&& arg) 3 { 4 TYPE* pRet = nullptr; 5 pRet = new TYPE(std::forward<ARG>(arg)); 6 return pRet; 7 }
嗯哼?難道把形參類型改為右值引用就可以了?懵圈....表示顛覆認知!看不懂無所謂,再往下看。
【2】引用折疊 與 模板推導
C++11是通過引入一條所謂“引用折疊”(reference collapsing)的新語言規則,並結合新的模板推導規則來實現的完美轉發。
先了解一下引用折疊。如下語句:
1 typedef const int T; 2 typedef T& TR; 3 TR& v = 10; // 該聲明在C++98中會導致編譯錯誤
在C++11之前,其中TR& v = 10;這樣的表達式會被編譯器認為是不合法的表達式。
而在C++11中,一旦出現了這樣的表達式,就會發生引用折疊,即將復雜的未知表達式折疊為已知的簡單表達式,
具體規則,如下圖所示:
個人覺得,這個規則還是得深刻理解,記住一個原則:一旦定義中出現了左值引用,引用折疊總是優先將其折疊為左值引用。
如下驗證程序:
1 #include <iostream> 2 using namespace std; 3 4 typedef const int T; 5 typedef T& TR; 6 typedef T&& TRR; 7 8 void JudgeType() 9 { 10 cout << "lvalue_ref_type?: " << is_lvalue_reference<TR>::value << endl; // 1 11 cout << "rvalue_ref_type?: " << is_rvalue_reference<TR>::value << endl; // 0 12 13 cout << "lvalue_ref_type?: " << is_lvalue_reference<TR&>::value << endl; // 1 14 cout << "rvalue_ref_type?: " << is_rvalue_reference<TR&>::value << endl; // 0 15 16 cout << "lvalue_ref_type?: " << is_lvalue_reference<TR&&>::value << endl; // 1 17 cout << "rvalue_ref_type?: " << is_rvalue_reference<TR&&>::value << endl; // 0 18 19 cout << "lvalue_ref_type?: " << is_lvalue_reference<TRR>::value << endl; // 0 20 cout << "rvalue_ref_type?: " << is_rvalue_reference<TRR>::value << endl; // 1 21 22 cout << "lvalue_ref_type?: " << is_lvalue_reference<TRR&>::value << endl; // 1 23 cout << "rvalue_ref_type?: " << is_rvalue_reference<TRR&>::value << endl; // 0 24 25 cout << "lvalue_ref_type?: " << is_lvalue_reference<TRR&&>::value << endl; // 0 26 cout << "rvalue_ref_type?: " << is_rvalue_reference<TRR&&>::value << endl; // 1 27 } 28 29 int main() 30 { 31 JudgeType(); 32 system("pause"); 33 }
模板推導。為了便於說明問題,下面我們把函數模板寫為如下形式:
1 template <typename T> 2 void IamForwording(T&& t) 3 { 4 IrunCodeActually(static_cast<T&&>(t)); 5 }
說明:IamForwording為轉發函數;IrunCodeActually為目標函數(即真正執行函數過程的目標)
模板對類型的推導規則就比較簡單:
當轉發函數的實參是類型X的一個左值引用,那么模板參數被推導為X&類型;
當轉發函數的實參是類型X的一個右值引用,那么模板的參數被推導為X&&類型。
再結合以上的引用折疊規則,就能確定出參數的實際類型。
尤其注意,我們不僅在參數部分使用了T&&這樣的標識,在目標函數傳參的強制類型轉換中也使用了這樣的形式。
這個標識不是右值引用,它有專用的名字為轉發引用(forwarding reference)。
比如,我們調用轉發函數時傳入了一個X類型的左值引用,可以想象,轉發函數將被實例化為如下形式:
1 void IamForwording(X& && t) 2 { 3 IrunCodeActually(static_cast<X& &&>(t)); 4 }
應用上引用折疊規則,就是:
1 void IamForwording(X& t) 2 { 3 IrunCodeActually(static_cast<X&>(t)); 4 }
如此一來,左值傳遞就毫無問題了。
實際使用的時候,IrunCodeActually如果接受左值引用的話,就可以直接調用轉發函數。
不過你可能會發現,這里調用前的static_cast沒有什么作用。
事實上,這里的static_cast是留給傳遞右值用的。如下分析。
如果我們調用轉發函數時傳入了一個X類型的右值引用的話,我們的轉發函數將被實例化為:
1 void IamForwording(X&& && t) 2 { 3 IrunCodeActually(static_cast<X&& &&>(t)); 4 }
應用上引用折疊規則,就是:
1 void IamForwording(X&& t) 2 { 3 IrunCodeActually(static_cast<X&&>(t)); 4 }
這里我們就看到了static_cast的重要性。
事實上,對於一個右值而言,當它使用右值引用表達式引用的時候,該右值引用卻是個不折不扣的左值。
那么我們想在函數調用中繼續傳遞右值,就需要使用std::move來進行左值向右值的轉換。
而std::move通常就是一個static_cast。不過在C++11中,用於完美轉發的函數卻不再叫作move,而是另外一個名字:forward。
move和forward在實際實現上差別並不大。
不過標准庫這么設計,也許是為了讓每個名字對應於不同的用途,以應對未來可能的擴展。
推薦在實現完美轉發時使用forward。
【3】完美轉發的應用
完美轉發的應用示例:
1 #include <iostream> 2 using namespace std; 3 4 void fun(int& x) { cout << "call lvalue ref" << endl; } 5 void fun(int&& x) { cout << "call rvalue ref" << endl; } 6 void fun(const int& x) { cout << "call const lvalue ref" << endl; } 7 void fun(const int&& x) { cout << "call const rvalue ref" << endl; } 8 9 template<typename T> 10 void PerfectForward(T&& t) 11 { 12 std::cout << "T is a ref type?: " << std::is_reference<T>::value << std::endl; 13 std::cout << "T is a lvalue ref type?: " << std::is_lvalue_reference<T>::value << std::endl; 14 std::cout << "T is a rvalue ref type?: " << std::is_rvalue_reference<T>::value << std::endl; 15 16 fun(forward<T>(t)); 17 } 18 19 int main() 20 { 21 PerfectForward(10); // call rvalue ref 22 23 int a = 5; 24 PerfectForward(a); // call lvalue ref 25 PerfectForward(move(a)); // call rvalue ref 26 27 const int b = 8; 28 PerfectForward(b); // call const lvalue ref 29 PerfectForward(move(b)); // call const rvalue ref 30 31 system("pause"); 32 return 0; 33 } 34 35 /* 36 T is a ref type?: 0 37 T is a lvalue ref type?: 0 38 T is a rvalue ref type?: 0 39 call rvalue ref 40 T is a ref type?: 1 41 T is a lvalue ref type?: 1 42 T is a rvalue ref type?: 0 43 call lvalue ref 44 T is a ref type?: 0 45 T is a lvalue ref type?: 0 46 T is a rvalue ref type?: 0 47 call rvalue ref 48 T is a ref type?: 1 49 T is a lvalue ref type?: 1 50 T is a rvalue ref type?: 0 51 call const lvalue ref 52 T is a ref type?: 0 53 T is a lvalue ref type?: 0 54 T is a rvalue ref type?: 0 55 call const rvalue ref 56 */
所有4種類型的值對完美轉發進行測試,可以看到,所有的轉發都被正確地送到了目的地。
注意分析,加深理解。
good good study, day day up.
順序 選擇 循環 總結