C++11完美轉發及實現方法詳解

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() 模板函數修飾被調用函數中需要維持左、右值屬性的參數。由此即可輕鬆實現函數模板中參數的完美轉發。 

圖片


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章