[C++中級進階]001_C++0x里的完美轉發到底是神馬?


 問題描述

   C++無疑是十分強大的,但是你可知道,在C++0x標准出現之前,在C++界里有一個十分棘手而未能解決的問題——參數轉發。問題的描述如下:

對於一個給定的函數E(a1, a2, ..., an),它有參數a1, a2, ..., an,你不可能寫出一個函數F(a1, a2, ..., an),使得該函數與E(a1, a2, ..., an)完全等價。

   對這個問題進而拆分,它有兩點:第一,函數F(a1, a2, ..., an)比如能夠接受任意的參數列表,並在不改變參數性質的前提下,將參數傳遞給E(a1, a2, ..., an);第二,函數F(a1, a2, ..., an)必須能夠將函數E(a1, a2, ..., an)的結果返回給自己的調用者。

   本文就第一點的參數轉發進行講解。

 標准解決方案的三大規則

   從更為嚴密的邏輯角度上來考慮轉發的問題,我們的轉發實現需要滿足下面三個條件。這里假設函數F(a1, a2, ..., an)調用函數G(a1, a2, ..., an)

C1. 對於能使用函數F(a1, a2, ..., an)的地方,函數G(a1, a2, ..., an)也一定能使用。

C2. 對於不能使用函數F(a1, a2, ..., an)的地方,函數G(a1, a2, ..., an)也一定不能使用。

C3. 實現函數F(a1, a2, ..., an)的時候,復雜度必須是線性增加的。(這一點乍看起來可能不理解,現在可以不用理解,后面有實例說明)

 七種轉發實現方案

   在實現轉發的方案里,有七種需要我們了解,本文會為你一一介紹,這七種轉發里,只有第七種能實現完美轉發。

   七種轉發中,1-4不需要對C++0x以前的標准進行修改,第五種轉發需要對標准關於參數推倒的規則進行修改,第六種和第七種都用到了C++0x標准里的右值引用。

 方案1. 非常量左值引用轉發

   何謂非常量左值引用呢?形如int& a;我們就把a叫做非常量左值引用。這里的左值引用就是我們平時使用的&。

   這個解決方案的實例代碼如下:

1 template<class A1, class A2, class A3> 
2 void f(A1 & a1, A2 & a2, A3 & a3)
3 {
4     return g(a1, a2, a3);
5 }
6 
7 void g(int a1, int a2, int a3){
8 
9 }

   轉發失敗原因:這個轉發不能傳入非常量右值,即下面的代碼是無法通過編譯的。

1 int main()
2 {
3     f(1, 2, 3);
4 }

   但是這種解決方案並不是一無是處的,對於那些只可能傳入左值的場景來說,比如Boost庫中的Iterator,這種應用還是能夠看到的。只是它並不是一個完美的轉發方案。

 方案2. 常量左值引用轉發

   何謂常量左值引用呢?形如const int& a;我們就把a叫做常量左值引用。

   這個解決方案的實例代碼如下:

1 template<class A1, class A2, class A3> 
2 void f(A1 const & a1, A2 const & a2, A3 const & a3)
3 {
4     return g(a1, a2, a3);
5 }
6 
7 void g(int& a1, int& a2, int& a3){
8  
9 }

   轉發失敗原因:上面的函數f雖然可以接受任何參數列表,但是當函數g接受非常量左值引用變量時,函數f是無法將常量左值引用參數傳遞給非常量左值引用的。

   這個解決方案一般用於拷貝構造函數,因為拷貝構造函數一般傳遞的都是形如const A&的參數,但也不排除有的拷貝構造函數比較變態,不傳遞const A&的參數。

 方案3. 非常量左值引用+常量左值引用轉發

   這個解決方案的實例代碼如下:

 1 template<class A1> 
 2 void f(A1 & a1)
 3 {
 4     return g(a1);
 5 }
 6 
 7 template<class A1> 
 8 void f(A1 const & a1)
 9 {
10     return g(a1);
11 }

   轉發失敗原因:上面的實現的確可以滿足所有的參數都能傳遞了,並且當函數g接受非常量參數時,編譯器也能找到最佳的匹配模板函數,即第一個。當然這個前提是所有的編譯器達成共識,認定第一個模板函數。除此之外,還有一個重要的問題,當函數的參數有三個的時候,我們不得不像下面這樣來實現我們的函數f:

 1 template<class A1, class A2, class A3> 
 2 void f(A1 const & a1, A2 const & a2, A3 const & a3)
 3 {
 4     return g(a1, a2, a3);
 5 }
 6 
 7 template<class A1, class A2, class A3> 
 8 void f(A1 & a1, A2 const & a2, A3 const & a3)
 9 {
10     return g(a1, a2, a3);
11 }
12 
13 template<class A1, class A2, class A3> 
14 void f(A1 const & a1, A2 & a2, A3 const & a3)
15 {
16     return g(a1, a2, a3);
17 }
18 
19 template<class A1, class A2, class A3> 
20 void f(A1 & a1, A2 & a2, A3 const & a3)
21 {
22     return g(a1, a2, a3);
23 }
24 
25 template<class A1, class A2, class A3> 
26 void f(A1 const & a1, A2 const & a2, A3 & a3)
27 {
28     return g(a1, a2, a3);
29 }
30 
31 template<class A1, class A2, class A3> 
32 void f(A1 & a1, A2 const & a2, A3 & a3)
33 {
34     return g(a1, a2, a3);
35 }
36 
37 template<class A1, class A2, class A3> 
38 void f(A1 const & a1, A2 & a2, A3 & a3)
39 {
40     return g(a1, a2, a3);
41 }
42 
43 template<class A1, class A2, class A3> 
44 void f(A1 & a1, A2 & a2, A3 & a3)
45 {
46     return g(a1, a2, a3);
47 }

   是的,這是指數級別的增長,這個就不符合我們的C3規則了,呵呵,這下理解C3規則是什么含義了吧。

 方案4. 常量左值引用+const_cast轉發

   const_cast的作用是什么呢?它可以去除常量的的const屬性,這個轉換可以解決方案2里遇到的問題。

   這個解決方案的實例代碼如下:

1 template<class A1, class A2, class A3> 
2 void f(A1 const & a1, A2 const & a2, A3 const & a3)
3 {
4     return g(const_cast<A1 &>(a1), const_cast<A2 &>(a2), const_cast<A3 &>(a3));
5 }

   轉發失敗原因:很顯然,去除了const屬性,我們就能修改原來的常量了,這樣的轉發會造成對常量的修改。這里讓我十分郁悶的是,C++既然提供了const,還非得提供一個const_cast,這是矛盾的存在。或許這就是C++的牛逼之處吧,哈哈。

 方案5. 非常量左值引用+修改的參數推倒規則轉發

   這里說明一下,我在看胡健的博客的時候,對“修改的參數推倒”這句話費透了腦筋。乍一看不懂,仔細看還是不懂,基礎不扎實可能吧。不過最后還是搞懂了,這里所謂的“修改參數推倒”是指修改C++的現有標准。在模板編程里,有一種參數推倒的說法。當你傳遞int類型的參數時,編譯器會為你找到最佳匹配的模板函數,然后再把參數傳遞給這個模板函數。在方案1里,導致我們失敗的事情就是無法傳遞非常量右值,但是如果修改C++標准,我們就能夠將非常量右值推倒成常量右值。

   但是,修改標准后,對於現有用C++實現的代碼造成十分巨大的破壞。具體參照如下代碼:

 1 template<class A1> 
 2 void f(A1 & a1)
 3 {
 4     std::cout << 1 << std::endl;
 5 }
 6 
 7 void f(long const &)
 8 {
 9     std::cout << 2 << std::endl;
10 }
11 
12 int main()
13 {
14     f(5);              // 在既有參數推倒規則,會打印2;修改參數推倒規則后,會打印1
15     int const n(5);
16     f(n);              // 這種情況好一點,都會打印1
17 }

   看出來對現有代碼的破壞了嗎?注意注釋。

 方案6. 右值引用轉發

   何謂右值引用呢?這是C++0x里新追加的特性,如果想更清楚一點,可以參考胡健的博客。

   這個解決方案的實例代碼如下:

1 template<class A1, class A2, class A3> 
2 void f(A1 && a1, A2 && a2, A3 && a3)
3 {
4     return g(a1, a2, a3);
5 }

   轉發失敗原因:函數g無法接收左值,因為不能將一個左值傳遞給一個右值引用。另外,當傳遞非常量右值時也會存在問題,因為此時a1、a2、a3本身是左值,這樣當F的參數是非常量左值引用時,我們就可以來修改傳入的非常量右值了,而右值是不能被修改的。

 完美方案7. 右值引用+修改的參數推倒規則轉發

   你可能疑惑,不是說過修改參數推倒規則后會導致對既有代碼的破壞嗎?是的,不過那是對左值參數推倒規則的修改,我們這里要修改的是針對右值引用推倒規則的修改。首先,要理解參數推倒規則,我們要理解引用疊加規則:

1、T& + &         = T&

2、T& + &&       = T&

3、T&& + &       = T&

4、T或T&& + && = T&&

   如何驗證上面的引用疊加規則呢?我們可以用下面這樣一段代碼來驗證這個問題:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 typedef int&  LRINT;
 5 typedef int&& RRINT;
 6 
 7 int main(){
 8 
 9     int     a = 10;
10 
11     // 左值引用
12     LRINT   b = a;      // 單純:&
13     LRINT&  c = a;      // 疊加:&  +  &   不能寫做:LRINT&  c = 10;    可見c是左值引用
14 
15     // 右值引用
16     RRINT    d = 10;    // 單純:&&
17     RRINT&&  e = 10;    // 疊加:&& + &&   不能寫做:RRINT&& e = a;     可見e是右值引用
18     LRINT&&  f = a;     // 疊加:&  + &&   不能寫做:LRINT&&  f = 10;   可見f是左值引用
19     RRINT&   g = a;     // 疊加:&& +  &   不能寫做:RRINT&   g = 10;   可見g是左值引用
20 
21     system("pause");
22     return 0;
23 }

   理解了引用疊加規則后,讓我們來看看修改后的針對右值引用的參數推倒規則:

   修改后的針對右值引用的參數推導規則為:若函數模板的模板參數為A,模板函數的形參為A&&,則可分為兩種情況討論:

1、若實參為T&,則模板參數A應被推導為引用類型T&。(由引用疊加規則第2點T& + && = T&和A&&=T&,可得出A=T&)

2、若實參為T&&,則模板參數A應被推導為非引用類型T。(由引用疊加規則第4點T或T&& + && = T&&和A&&=T&&,可得出A=T或T&&,強制規定A=T)

   應用了新的參數推導規則后,我們來看下面的代碼:

1 template<class A1> 
2 void f(A1 && a1)
3 {
4     return g(static_cast<A1 &&>(a1));
5 }

   當傳給f一個左值(類型為T)時,由於模板是一個引用類型,因此它被隱式裝換為左值引用類型T&,根據推導規則1,模板參數A被推導為T&。這樣,在f內部調用F(static_cast<A &&>(a))時,static_cast<A &&>(a)等同於static_cast<T& &&>(a),根據引用疊加規則第2點,即為static_cast<T&>(a),這樣轉發給g的還是一個左值。

   當傳給f一個右值(類型為T)時,由於模板是一個引用類型,因此它被隱式裝換為右值引用類型T&&,根據推導規則2,模板參數A被推導為T。這樣,在G內部調用F(static_cast<A &&>(a))時,static_cast<A &&>(a)等同於static_cast<T&&>(a),這樣轉發給F的還是一個右值(不具名右值引用是右值)。

   可見,使用該方案后,左值和右值都能正確地進行轉發,並且不會帶來其他問題。

 參考資料

   1.【原】C++ 11完美轉發   http://www.cnblogs.com/hujian/archive/2012/02/17/2355207.html

   2.【原】C++ 11右值引用   http://www.cnblogs.com/hujian/archive/2012/02/13/2348621.html

   3. The Forwarding Problem: Arguments  http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2002/n1385.htm

   4. A Proposal to Add an Rvalue Reference to the C++ Language  http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2004/n1690.html 

   5. A Brief Introduction to Rvalue References  http://www.artima.com/cppsource/rvalue.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM