【C/C++開發】C++11:右值引用和轉發型引用

右值引用

爲了解決移動語義及完美轉發問題,C++11標準引入了右值引用(rvalue reference)這一重要的新概念。右值引用採用T&&這一語法形式,比傳統的引用T&(現在被稱做左值引用 lvalue reference)多一個&。
若是把經由T&&這一語法形式所產生的引用類型都叫作右值引用,那麼這種廣義的右值引用又可分爲如下三種類型:
  • 無名右值引用
  • 具名右值引用
  • 轉發型引用
無名右值引用和具名右值引用的引入主要是爲了解決移動語義問題。
轉發型引用的引入主要是爲了解決完美轉發問題。
 

無名右值引用

無名右值引用(unnamed rvalue reference)是指由右值引用相關操做所產生的引用類型。
無名右值引用主要經過返回右值引用的類型轉換操做產生, 其語法形式以下:
static_cast<T&&>(t)
標準規定該語法形式將把表達式 t 轉換爲T類型的無名右值引用。
無名右值引用是右值,標準規定無名右值引用和傳統的右值同樣具備潛在的可移動性,即它所佔有的資源能夠被移動(竊取)。
 

std::move()

因爲無名右值引用是右值,藉助於類型轉換操做產生無名右值引用這一手段,左值表達式就能夠被轉換成右值表達式。爲了便於利用這一重要的轉換操做,標準庫爲咱們提供了封裝這一操做的函數,這就是std::move()。
假設左值表達式 t 的類型爲T&,利用如下函數調用就能夠把左值表達式 t 轉換爲T類型的無名右值引用(右值,類型爲T&&)。
std::move(t)
 

具名右值引用

若是某個變量或參數被聲明爲T&&類型,而且T無需推導便可肯定,那麼這個變量或參數就是一個具名右值引用(named rvalue reference)。
具名右值引用是左值,由於具名右值引用有名字,和傳統的左值引用同樣能夠用操做符&取地址。
與廣義的右值引用相對應,狹義的右值引用僅限指具名右值引用。
傳統的左值引用能夠綁定左值,在某些狀況下也可綁定右值。與此不一樣的是,右值引用只能綁定右值。
右值引用和左值引用統稱爲引用(reference),它們具備引用的共性,好比都必須在初始化時綁定值,都是左值等等。
[cpp]  view plain  copy
 print ?
  1. struct X {};  
  2. X a;  
  3. X&& b = static_cast<X&&>(a);  
  4. X&& c = std::move(a);  
  5. //static_cast<X&&>(a) 和 std::move(a) 是無名右值引用,是右值  
  6. //b 和 c 是具名右值引用,是左值  
  7. X& d = a;  
  8. X& e = b;  
  9. const X& f = c;  
  10. const X& g = X();  
  11. X&& h = X();  
  12. //左值引用d和e只能綁定左值(包括傳統左值:變量a以及新型左值:右值引用b)  
  13. //const左值引用f和g能夠綁定左值(右值引用c),也能夠綁定右值(臨時對象X())  
  14. //右值引用b,c和h只能綁定右值(包括新型右值:無名右值引用std::move(a)以及傳統右值:臨時對象X())  
 

左右值重載策略

有時咱們須要在函數中區分參數的左右值屬性,根據參數左右值屬性的不一樣作出不一樣的處理。適當地採用左右值重載策略,藉助於左右值引用參數不一樣的綁定特性,咱們能夠利用函數重載來作到這一點。常見的左右值重載策略以下:
[cpp]  view plain  copy
 print ?
  1. struct X {};  
  2. //左值版本  
  3. void f(const X& param1){/*處理左值參數param1*/}  
  4. //右值版本  
  5. void f(X&& param2){/*處理右值參數param2*/}  
  6.   
  7. X a;  
  8. f(a);            //調用左值版本  
  9. f(X());          //調用右值版本  
  10. f(std::move(a)); //調用右值版本  
即在函數重載中分別重載const左值引用和右值引用。
重載const左值引用的爲左值版本,這是由於const左值引用參數能綁定左值,而右值引用參數不能綁定左值。
重載右值引用的爲右值版本,這是由於雖然const左值引用參數和右值引用參數都能綁定右值,但標準規定右值引用參數的綁定優先度要高於const左值引用參數。
 

移動構造器和移動賦值運算符

在類的構造器和賦值運算符中運用上述左右值重載策略,就會產生兩個新的特殊成員函數:移動構造器(move constructor)和移動賦值運算符(move assignment operator)。
[cpp]  view plain  copy
 print ?
  1. struct X  
  2. {  
  3.     X();                         //缺省構造器  
  4.     X(const X& that);            //拷貝構造器  
  5.     X(X&& that);                 //移動構造器  
  6.     X& operator=(const X& that); //拷貝賦值運算符  
  7.     X& operator=(X&& that);      //移動賦值運算符  
  8. };  
  9.   
  10. X a;                             //調用缺省構造器  
  11. X b = a;                         //調用拷貝構造器  
  12. X c = std::move(b);              //調用移動構造器  
  13. b = a;                           //調用拷貝賦值運算符  
  14. c = std::move(b);                //調用移動賦值運算符  
 

移動語義

無名右值引用和具名右值引用的引入主要是爲了解決移動語義問題。
移動語義問題是指在某些特定狀況下(好比用右值來賦值或構造對象時)如何採用廉價的移動語義替換昂貴的拷貝語義的問題。
移動語義(move semantics)是指某個對象接管另外一個對象所擁有的外部資源的全部權。移動語義須要經過移動(竊取)其餘對象所擁有的資源來完成。移動語義的具體實現(即一次that對象到this對象的移動(move))一般包含如下若干步驟:
  • 若是this對象自身也擁有資源,釋放該資源
  • 將this對象的指針或句柄指向that對象所擁有的資源
  • 將that對象本來指向該資源的指針或句柄設爲空值
上述步驟可簡單歸納爲①釋放this(this非空時)②移動that
移動語義一般在移動構造器和移動賦值運算符中得以具體實現。二者的區別在於移動構造對象時this對象爲空於是①釋放this無須進行。

與移動語義相對,傳統的拷貝語義(copy semantics)是指某個對象拷貝(複製)另外一個對象所擁有的外部資源並得到新生資源的全部權。拷貝語義的具體實現(即一次that對象到this對象的拷貝(copy))一般包含如下若干步驟:
  • 若是this對象自身也擁有資源,釋放該資源
  • 拷貝(複製)that對象所擁有的資源
  • 將this對象的指針或句柄指向新生的資源
  • 若是that對象爲臨時對象(右值),那麼拷貝完成以後that對象所擁有的資源將會因that對象被銷燬而即刻得以釋放
上述步驟可簡單歸納爲①釋放this(this非空時)②拷貝that③釋放that(that爲右值時)
拷貝語義一般在拷貝構造器和拷貝賦值運算符中得以具體實現。二者的區別在於拷貝構造對象時this對象爲空於是①釋放this無須進行。

比較移動語義與拷貝語義的具體步驟可知,在賦值或構造對象時,
  • 若是源對象that爲左值,因爲二者效果不一樣(移動that ≠ 拷貝that),此時移動語義不能用來替換拷貝語義。
  • 若是源對象that爲右值,因爲二者效果相同(移動that = 拷貝that + 釋放that),此時廉價的移動語義(經過指針操做來移動資源)即可以用來替換昂貴的拷貝語義(生成,拷貝而後釋放資源)。
由此可知,只要在進行相關操做(好比賦值或構造)時,採起適當的左右值重載策略區分源對象的左右值屬性,根據其左右值屬性分別採用拷貝語義和移動語義,移動語義問題即可以獲得解決。

下面用MemoryBlock這個自我管理內存塊的類來具體說明移動語義問題。
[cpp]  view plain  copy
 print ?
  1. #include <iostream>  
  2.   
  3. class MemoryBlock  
  4. {  
  5. public:  
  6.   
  7.     // 構造器(初始化資源)  
  8.     explicit MemoryBlock(size_t length)  
  9.         : _length(length)  
  10.         , _data(new int[length])  
  11.     {  
  12.     }  
  13.   
  14.     // 析構器(釋放資源)  
  15.     ~MemoryBlock()  
  16.     {  
  17.         if (_data != nullptr)  
  18.         {  
  19.             delete[] _data;  
  20.         }  
  21.     }  
  22.   
  23.     // 拷貝構造器(實現拷貝語義:拷貝that)  
  24.     MemoryBlock(const MemoryBlock& that)  
  25.         // 拷貝that對象所擁有的資源  
  26.         : _length(that._length)  
  27.         , _data(new int[that._length])  
  28.     {  
  29.         std::copy(that._data, that._data + _length, _data);  
  30.     }  
  31.   
  32.     // 拷貝賦值運算符(實現拷貝語義:釋放this + 拷貝that)  
  33.     MemoryBlock& operator=(const MemoryBlock& that)  
  34.     {  
  35.         if (this != &that)  
  36.         {  
  37.             // 釋放自身的資源  
  38.             delete[] _data;  
  39.   
  40.             // 拷貝that對象所擁有的資源  
  41.             _length = that._length;  
  42.             _data = new int[_length];  
  43.             std::copy(that._data, that._data + _length, _data);  
  44.         }  
  45.         return *this;  
  46.     }  
  47.   
  48.     // 移動構造器(實現移動語義:移動that)  
  49.     MemoryBlock(MemoryBlock&& that)  
  50.         // 將自身的資源指針指向that對象所擁有的資源  
  51.         : _length(that._length)  
  52.         , _data(that._data)  
  53.     {  
  54.         // 將that對象本來指向該資源的指針設爲空值  
  55.         that._data = nullptr;  
  56.         that._length = 0;  
  57.     }  
  58.   
  59.     // 移動賦值運算符(實現移動語義:釋放this + 移動that)  
  60.     MemoryBlock& operator=(MemoryBlock&& that)  
  61.     {  
  62.         if (this != &that)  
  63.         {  
  64.             // 釋放自身的資源  
  65.             delete[] _data;  
  66.   
  67.             // 將自身的資源指針指向that對象所擁有的資源  
  68.             _data = that._data;  
  69.             _length = that._length;  
  70.   
  71.             // 將that對象本來指向該資源的指針設爲空值  
  72.             that._data = nullptr;  
  73.             that._length = 0;  
  74.         }  
  75.         return *this;  
  76.     }  
  77. private:  
  78.     size_t _length; // 資源的長度  
  79.     int* _data; // 指向資源的指針,表明資源自己  
  80. };  
  81.   
  82. MemoryBlock f() { return MemoryBlock(50); }  
  83.   
  84. int main()  
  85. {  
  86.     MemoryBlock a = f();            // 調用移動構造器,移動語義  
  87.     MemoryBlock b = a;              // 調用拷貝構造器,拷貝語義  
  88.     MemoryBlock c = std::move(a);   // 調用移動構造器,移動語義  
  89.     a = f();                        // 調用移動賦值運算符,移動語義  
  90.     b = a;                          // 調用拷貝賦值運算符,拷貝語義  
  91.     c = std::move(a);               // 調用移動賦值運算符,移動語義  
  92. }  
 

轉發型引用

若是某個變量或參數被聲明爲T&&類型,而且T須要通過推導纔可肯定,那麼這個變量或參數就是一個轉發型引用(forwarding reference)。
轉發型引用由如下兩種語法形式產生
  • 若是某個變量被聲明爲auto&&類型,那麼這個變量就是一個轉發型引用
  • 在函數模板中,若是某個參數被聲明爲T&&類型,而且T是一個須要通過推導纔可肯定的模板參數類型,那麼這個參數就是一個轉發型引用
轉發型引用是不穩定的,它的實際類型由它所綁定的值來肯定。轉發型引用既能夠綁定左值,也能夠綁定右值。若是綁定左值,轉發型引用就成了左值引用。若是綁定右值,轉發型引用就成了右值引用。
轉發型引用在被C++標準所認可以前曾經被稱做萬能引用(universal reference)。萬能引用這一術語的發明者,Effective C++系列的做者Scott Meyers認爲,如此異常靈活的引用類型不屬於右值引用,它應該擁有本身的名字。

對於某個轉發型引用類型的變量(auto&&類型)來講
  • 若是初始化表達式爲左值(類型爲U&),該變量將成爲左值引用(類型爲U&)。
  • 若是初始化表達式爲右值(類型爲U&&),該變量將成爲右值引用(類型爲U&&)。
對於函數模板中的某個轉發型引用類型的形參(T&&類型)來講
  • 若是對應的實參爲左值(類型爲U&),模板參數T將被推導爲引用類型U&,該形參將成爲左值引用(類型爲U&)。
  • 若是對應的實參爲右值(類型爲U&&),模板參數T將被推導爲非引用類型U,該形參將成爲右值引用(類型爲U&&)。
[cpp]  view plain  copy
 print ?
  1. struct X {};  
  2. X&& var1 = X();                            // var1是右值引用,只能綁定右值X()  
  3. auto&& var2 = var1;                        // var2是轉發型引用,能夠綁定左值var1  
  4.                                            // var2的實際類型等同於左值var1,即X&  
  5. auto&& var3 = X();                         // var3是轉發型引用,能夠綁定右值X()  
  6.                                            // var3的實際類型等同於右值X(),即X&&  
  7. template<typename T>  
  8. void g(std::vector<typename T>&& param1);  // param1是右值引用  
  9. template<typename T>  
  10. void f(T&& param2);                        // param2是轉發型引用  
  11.   
  12. X a;  
  13. f(a);                // 模板函數f()的形參param2是轉發型引用,能夠綁定左值a  
  14.                      // 在這次調用中模板參數T將被推導爲引用類型X&  
  15.                      // 而形參param2的實際類型將等同於左值a,即X&  
  16. f(X());              // 模板函數f()的形參param2是轉發型引用,能夠綁定右值X()  
  17.                      // 在這次調用中模板參數T將被推導爲非引用類型X  
  18.                      // 而形參param2的實際類型將等同於右值X(),即X&&  
  19.   
  20. // 更多右值引用和轉發型引用  
  21. const auto&& var4 = 10;                           // 右值引用  
  22. template<typename T>  
  23. void h(const T&& param1);                         // 右值引用  
  24. template <typename T/*, class Allocator = allocator*/>  
  25. class vector  
  26. {  
  27. public:  
  28.     void push_back( T&& t );                      // 右值引用  
  29.     template <typename Args...>  
  30.     void emplace_back( Args&&... args );          // 轉發型引用  
  31. };  
 

完美轉發

完美轉發(perfect forwarding)問題是指函數模板在向其餘函數轉發(傳遞)自身參數(形參)時該如何保留該參數(實參)的左右值屬性的問題。也就是說函數模板在向其餘函數轉發(傳遞)自身形參時,若是相應實參是左值,它就應該被轉發爲左值;一樣若是相應實參是右值,它就應該被轉發爲右值。這樣作是爲了保留在其餘函數針對轉發而來的參數的左右值屬性進行不一樣處理(好比參數爲左值時實施拷貝語義;參數爲右值時實施移動語義)的可能性。若是將自身參數不分左右值一概轉發爲左值,其餘函數就只能將轉發而來的參數視爲左值,從而失去針對該參數的左右值屬性進行不一樣處理的可能性。

轉發型引用的引入主要是爲了解決完美轉發問題。在函數模板中須要保留左右值屬性的參數,也就是要被完美轉發的參數須被聲明爲轉發型引用類型,即參數必須被聲明爲T&&類型,而T必須被包含在函數模板的模板參數列表之中。按照轉發型引用類型形參的特色,該形參將根據所對應的實參的左右值屬性而分別蛻變成左右值引用。但不管該形參成爲左值引用仍是右值引用,該形參在函數模板內都將成爲左值。這是由於該形參有名字,左值引用是左值,具名右值引用也一樣是左值。若是在函數模板內照原樣轉發該形參,其餘函數就只能將轉發而來的參數視爲左值,完美轉發任務將會失敗。
[cpp]  view plain  copy
 print ?
  1. #include<iostream>  
  2. using namespace std;  
  3.   
  4. struct X {};  
  5. void inner(const X&) {cout << "inner(const X&)" << endl;}  
  6. void inner(X&&) {cout << "inner(X&&)" << endl;}  
  7. template<typename T>  
  8. void outer(T&& t) {inner(t);}  
  9.   
  10. int main()  
  11. {  
  12.     X a;  
  13.     outer(a);  
  14.     outer(X());  
  15. }  
  16. //inner(const X&)  
  17. //inner(const X&)  

std::forward()

要在函數模板中完成完美轉發轉發型引用類型形參的任務,咱們必須在相應實參爲左值,該形參成爲左值引用時把它轉發成左值,在相應實參爲右值,該形參成爲右值引用時把它轉發成右值。此時咱們須要標準庫函數std::forward()。
標準庫函數 std::forward<T>(t) 有兩個參數:模板參數 T 與 函數參數 t。函數功能以下:
  • 當T爲左值引用類型U&時,t 將被轉換爲無名左值引用(左值,類型爲U&)。
  • 當T爲非引用類型U或右值引用類型U&&時,t 將被轉換爲無名右值引用(右值,類型爲U&&)。
使用此函數,咱們在函數模板中轉發類型爲T&&的轉發型引用參數 t 時,只需將參數 t 替換爲std::forward<T>(t)便可完成完美轉發任務。這是由於
  • 若是 t 對應的實參爲左值(類型爲U&),模板參數T將被推導爲引用類型U&,t 成爲具名左值引用(類型爲U&),std::forward<T>(t)就會把 t 轉換成無名左值引用(左值,類型爲U&)。
  • 若是 t 對應的實參爲右值(類型爲U&&),模板參數T將被推導爲非引用類型U,t 成爲具名右值引用(類型爲U&&),std::forward<T>(t)就會把 t 轉換成無名右值引用(右值,類型爲U&&)。
[cpp]  view plain  copy
 print ?
  1. #include<iostream>  
  2. using namespace std;  
  3.   
  4. struct X {};  
  5. void inner(const X&) {cout << "inner(const X&)" << endl;}  
  6. void inner(X&&) {cout << "inner(X&&)" << endl;}  
  7. template<typename T>  
  8. void outer(T&& t) {inner(forward<T>(t));}  
  9.   
  10. int main()  
  11. {  
  12.     X a;  
  13.     outer(a);  
  14.     outer(X());  
  15. }  
  16. //inner(const X&)  
  17. //inner(X&&)
相關文章
相關標籤/搜索