1. 程式人生 > 其它 >C++11 STL 完美轉發

C++11 STL 完美轉發

http://m.biancheng.net/view/7868.html

C++11 標準為 C++ 引入右值引用語法的同時,還解決了一個 C++ 98/03 標準長期存在的短板,即使用簡單的方式即可在函式模板中實現引數的完美轉發。那麼,什麼是完美轉發?它為什麼是 C++98/03 標準存在的一個短板?C++11 標準又是如何為 C++ 彌補這一短板的?別急,本節將就這些問題給讀者做一一講解。

首先解釋一下什麼是完美轉發,它指的是函式模板可以將自己的引數“完美”地轉發給內部呼叫的其它函式。所謂完美,即不僅能準確地轉發引數的值,還能保證被轉發引數的左、右值屬性不變。

在 C++ 中,一個表示式不是左值就是右值。有關如何判斷一個表示式是左值還是右值,可閱讀《C++右值引用》一文做詳細瞭解。

舉個例子:

    template<typename T>
    void function(T t) {
        otherdef(t);
    }

如上所示,function() 函式模板中呼叫了 otherdef() 函式。在此基礎上,完美轉發指的是:如果 function() 函式接收到的引數 t 為左值,那麼該函式傳遞給 otherdef() 的引數 t 也是左值;反之如果 function() 函式接收到的引數 t 為右值,那麼傳遞給 otherdef() 函式的引數 t 也必須為右值。

顯然,function() 函式模板並沒有實現完美轉發。一方面,引數 t 為非引用型別,這意味著在呼叫 function() 函式時,實參將值傳遞給形參的過程就需要額外進行一次拷貝操作;另一方面,無論呼叫 function() 函式模板時傳遞給引數 t 的是左值還是右值,對於函式內部的引數 t 來說,它有自己的名稱,也可以獲取它的儲存地址,因此它永遠都是左值,也就是說,傳遞給 otherdef() 函式的引數 t 永遠都是左值。總之,無論從那個角度看,function() 函式的定義都不“完美”。

    讀者可能會問,完美轉發這樣嚴苛的引數傳遞機制,很常用嗎?C++98/03 標準中幾乎不會用到,但 C++11 標準為 C++ 引入了右值引用和移動語義,因此很多場景中是否實現完美轉發,直接決定了該引數的傳遞過程使用的是拷貝語義(呼叫拷貝建構函式)還是移動語義(呼叫移動建構函式)。


事實上,C++98/03 標準下的 C++ 也可以實現完美轉發,只是實現方式比較笨拙。通過前面的學習我們知道,C++ 98/03 標準中只有左值引用,並且可以細分為非 const 引用和 const 引用。其中,使用非 const 引用作為函式模板引數時,只能接收左值,無法接收右值;而 const 左值引用既可以接收左值,也可以接收右值,但考慮到其 const 屬性,除非被呼叫函式的引數也是 const 屬性,否則將無法直接傳遞。

這也就意味著,單獨使用任何一種引用形式,可以實現轉發,但無法保證完美。因此如果使用 C++ 98/03 標準下的 C++ 語言,我們可以採用函式模板過載的方式實現完美轉發,例如:

    #include <iostream>
    using namespace std;
    //過載被呼叫函式,檢視完美轉發的效果
    void otherdef(int & t) {
        cout << "lvalue\n";
    }
    void otherdef(const int & t) {
        cout << "rvalue\n";
    }
    //過載函式模板,分別接收左值和右值
    //接收右值引數
    template <typename T>
    void function(const T& t) {
        otherdef(t);
    }
    //接收左值引數
    template <typename T>
    void function(T& t) {
        otherdef(t);
    }
    int main()
    {
        function(5);//5 是右值
        int  x = 1;
        function(x);//x 是左值
        return 0;
    }

程式執行結果為:

rvalue
lvalue
從輸出結果中可以看到,對於右值 5 來說,它實際呼叫的引數型別為 const T& 的函式模板,由於 t 為 const 型別,所以 otherdef() 函式實際呼叫的也是引數用 const 修飾的函式,所以輸出“rvalue”;對於左值 x 來說,2 個過載模板函式都適用,C++編譯器會選擇最適合的引數型別為 T& 的函式模板,進而 therdef() 函式實際呼叫的是引數型別為非 const 的函式,輸出“lvalue”。

顯然,使用過載的模板函式實現完美轉發也是有弊端的,此實現方式僅適用於模板函式僅有少量引數的情況,否則就需要編寫大量的過載函式模板,造成程式碼的冗餘。為了方便使用者更快速地實現完美轉發,C++ 11 標準中允許在函式模板中使用右值引用來實現完美轉發。

C++11標準中規定,通常情況下右值引用形式的引數只能接收右值,不能接收左值。但對於函式模板中使用右值引用語法定義的引數來說,它不再遵守這一規定,既可以接收右值,也可以接收左值(此時的右值引用又被稱為“萬能引用”)。

仍以 function() 函式為例,在 C++11 標準中實現完美轉發,只需要編寫如下一個模板函式即可:

    template <typename T>
    void function(T&& t) {
        otherdef(t);
    }

此模板函式的引數 t 既可以接收左值,也可以接收右值。但僅僅使用右值引用作為函式模板的引數是遠遠不夠的,還有一個問題繼續解決,即如果呼叫 function() 函式時為其傳遞一個左值引用或者右值引用的實參,如下所示:

    int n = 10;
    int & num = n;
    function(num); // T 為 int&
    int && num2 = 11;
    function(num2); // T 為 int &&

其中,由 function(num) 例項化的函式底層就變成了 function(int & & t),同樣由 function(num2) 例項化的函式底層則變成了 function(int && && t)。要知道,C++98/03 標準是不支援這種用法的,而 C++ 11標準為了更好地實現完美轉發,特意為其指定了新的型別匹配規則,又稱為引用摺疊規則(假設用 A 表示實際傳遞引數的型別):

    當實參為左值或者左值引用(A&)時,函式模板中 T&& 將轉變為 A&(A& && = A&);
    當實參為右值或者右值引用(A&&)時,函式模板中 T&& 將轉變為 A&&(A&& && = A&&)。

    讀者只需要知道,在實現完美轉發時,只要函式模板的引數型別為 T&&,則 C++ 可以自行準確地判定出實際傳入的實參是左值還是右值。

通過將函式模板的形參型別設定為 T&&,我們可以很好地解決接收左、右值的問題。但除此之外,還需要解決一個問題,即無論傳入的形參是左值還是右值,對於函式模板內部來說,形參既有名稱又能定址,因此它都是左值。那麼如何才能將函式模板接收到的形參連同其左、右值屬性,一起傳遞給被呼叫的函式呢?

C++11 標準的開發者已經幫我們想好的解決方案,該新標準還引入了一個模板函式 forword<T>(),我們只需要呼叫該函式,就可以很方便地解決此問題。仍以 function 模板函式為例,如下演示了該函式模板的用法:

    #include <iostream>
    using namespace std;
    //過載被呼叫函式,檢視完美轉發的效果
    void otherdef(int & t) {
        cout << "lvalue\n";
    }
    void otherdef(const int & t) {
        cout << "rvalue\n";
    }
    //實現完美轉發的函式模板
    template <typename T>
    void function(T&& t) {
        otherdef(forward<T>(t));
    }
    int main()
    {
        function(5);
        int  x = 1;
        function(x);
        return 0;
    }

程式執行結果為:

rvalue
lvalue
注意程式中第 12~16 行,此 function() 模板函式才是實現完美轉發的最終版本。可以看到,forword() 函式模板用於修飾被呼叫函式中需要維持引數左、右值屬性的引數。

總的來說,在定義模板函式時,我們採用右值引用的語法格式定義引數型別,由此該函式既可以接收外界傳入的左值,也可以接收右值;其次,還需要使用 C++11 標準庫提供的 forword() 模板函式修飾被呼叫函式中需要維持左、右值屬性的引數。由此即可輕鬆實現函式模板中引數的完美轉發。