您好,登錄后才能下訂單哦!
這篇文章主要為大家展示了“C++11中右值引用和轉發型引用的示例分析”,內容簡而易懂,條理清晰,希望能夠幫助大家解決疑惑,下面讓小編帶領大家一起研究并學習一下“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),它們具有引用的共性,比如都必須在初始化時綁定值,都是左值等等。
struct X {}; X a; X&& b = static_cast<X&&>(a); X&& c = std::move(a); //static_cast<X&&>(a) 和 std::move(a) 是無名右值引用,是右值 //b 和 c 是具名右值引用,是左值 X& d = a; X& e = b; const X& f = c; const X& g = X(); X&& h = X(); //左值引用d和e只能綁定左值(包括傳統左值:變量a以及新型左值:右值引用b) //const左值引用f和g可以綁定左值(右值引用c),也可以綁定右值(臨時對象X()) //右值引用b,c和h只能綁定右值(包括新型右值:無名右值引用std::move(a)以及傳統右值:臨時對象X())
左右值重載策略
有時我們需要在函數中區分參數的左右值屬性,根據參數左右值屬性的不同做出不同的處理。適當地采用左右值重載策略,借助于左右值引用參數不同的綁定特性,我們可以利用函數重載來做到這一點。常見的左右值重載策略如下:
struct X {}; //左值版本 void f(const X& param1){/*處理左值參數param1*/} //右值版本 void f(X&& param2){/*處理右值參數param2*/} X a; f(a); //調用左值版本 f(X()); //調用右值版本 f(std::move(a)); //調用右值版本
即在函數重載中分別重載const左值引用和右值引用。
重載const左值引用的為左值版本,這是因為const左值引用參數能綁定左值,而右值引用參數不能綁定左值。
重載右值引用的為右值版本,這是因為雖然const左值引用參數和右值引用參數都能綁定右值,但標準規定右值引用參數的綁定優先度要高于const左值引用參數。
移動構造器和移動賦值運算符
在類的構造器和賦值運算符中運用上述左右值重載策略,就會產生兩個新的特殊成員函數:移動構造器(move constructor)和移動賦值運算符(move assignment operator)。
struct X { X(); //缺省構造器 X(const X& that); //拷貝構造器 X(X&& that); //移動構造器 X& operator=(const X& that); //拷貝賦值運算符 X& operator=(X&& that); //移動賦值運算符 }; X a; //調用缺省構造器 X b = a; //調用拷貝構造器 X c = std::move(b); //調用移動構造器 b = a; //調用拷貝賦值運算符 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這個自我管理內存塊的類來具體說明移動語義問題。
#include <iostream> class MemoryBlock { public: // 構造器(初始化資源) explicit MemoryBlock(size_t length) : _length(length) , _data(new int[length]) { } // 析構器(釋放資源) ~MemoryBlock() { if (_data != nullptr) { delete[] _data; } } // 拷貝構造器(實現拷貝語義:拷貝that) MemoryBlock(const MemoryBlock& that) // 拷貝that對象所擁有的資源 : _length(that._length) , _data(new int[that._length]) { std::copy(that._data, that._data + _length, _data); } // 拷貝賦值運算符(實現拷貝語義:釋放this + 拷貝that) MemoryBlock& operator=(const MemoryBlock& that) { if (this != &that) { // 釋放自身的資源 delete[] _data; // 拷貝that對象所擁有的資源 _length = that._length; _data = new int[_length]; std::copy(that._data, that._data + _length, _data); } return *this; } // 移動構造器(實現移動語義:移動that) MemoryBlock(MemoryBlock&& that) // 將自身的資源指針指向that對象所擁有的資源 : _length(that._length) , _data(that._data) { // 將that對象原本指向該資源的指針設為空值 that._data = nullptr; that._length = 0; } // 移動賦值運算符(實現移動語義:釋放this + 移動that) MemoryBlock& operator=(MemoryBlock&& that) { if (this != &that) { // 釋放自身的資源 delete[] _data; // 將自身的資源指針指向that對象所擁有的資源 _data = that._data; _length = that._length; // 將that對象原本指向該資源的指針設為空值 that._data = nullptr; that._length = 0; } return *this; } private: size_t _length; // 資源的長度 int* _data; // 指向資源的指針,代表資源本身 }; MemoryBlock f() { return MemoryBlock(50); } int main() { MemoryBlock a = f(); // 調用移動構造器,移動語義 MemoryBlock b = a; // 調用拷貝構造器,拷貝語義 MemoryBlock c = std::move(a); // 調用移動構造器,移動語義 a = f(); // 調用移動賦值運算符,移動語義 b = a; // 調用拷貝賦值運算符,拷貝語義 c = std::move(a); // 調用移動賦值運算符,移動語義 }
轉發型引用
如果某個變量或參數被聲明為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&&)。
struct X {}; X&& var1 = X(); // var1是右值引用,只能綁定右值X() auto&& var2 = var1; // var2是轉發型引用,可以綁定左值var1 // var2的實際類型等同于左值var1,即X& auto&& var3 = X(); // var3是轉發型引用,可以綁定右值X() // var3的實際類型等同于右值X(),即X&& template<typename T> void g(std::vector<typename T>&& param1); // param1是右值引用 template<typename T> void f(T&& param2); // param2是轉發型引用 X a; f(a); // 模板函數f()的形參param2是轉發型引用,可以綁定左值a // 在此次調用中模板參數T將被推導為引用類型X& // 而形參param2的實際類型將等同于左值a,即X& f(X()); // 模板函數f()的形參param2是轉發型引用,可以綁定右值X() // 在此次調用中模板參數T將被推導為非引用類型X // 而形參param2的實際類型將等同于右值X(),即X&& // 更多右值引用和轉發型引用 const auto&& var4 = 10; // 右值引用 template<typename T> void h(const T&& param1); // 右值引用 template <typename T/*, class Allocator = allocator*/> class vector { public: void push_back( T&& t ); // 右值引用 template <typename Args...> void emplace_back( Args&&... args ); // 轉發型引用 };
完美轉發
完美轉發(perfect forwarding)問題是指函數模板在向其他函數轉發(傳遞)自身參數(形參)時該如何保留該參數(實參)的左右值屬性的問題。也就是說函數模板在向其他函數轉發(傳遞)自身形參時,如果相應實參是左值,它就應該被轉發為左值;同樣如果相應實參是右值,它就應該被轉發為右值。這樣做是為了保留在其他函數針對轉發而來的參數的左右值屬性進行不同處理(比如參數為左值時實施拷貝語義;參數為右值時實施移動語義)的可能性。如果將自身參數不分左右值一律轉發為左值,其他函數就只能將轉發而來的參數視為左值,從而失去針對該參數的左右值屬性進行不同處理的可能性。
轉發型引用的引入主要是為了解決完美轉發問題。在函數模板中需要保留左右值屬性的參數,也就是要被完美轉發的參數須被聲明為轉發型引用類型,即參數必須被聲明為T&&類型,而T必須被包含在函數模板的模板參數列表之中。按照轉發型引用類型形參的特點,該形參將根據所對應的實參的左右值屬性而分別蛻變成左右值引用。但無論該形參成為左值引用還是右值引用,該形參在函數模板內都將成為左值。這是因為該形參有名字,左值引用是左值,具名右值引用也同樣是左值。如果在函數模板內照原樣轉發該形參,其他函數就只能將轉發而來的參數視為左值,完美轉發任務將會失敗。
#include<iostream> using namespace std; struct X {}; void inner(const X&) {cout << "inner(const X&)" << endl;} void inner(X&&) {cout << "inner(X&&)" << endl;} template<typename T> void outer(T&& t) {inner(t);} int main() { X a; outer(a); outer(X()); } //inner(const X&) //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&&)。
#include<iostream> using namespace std; struct X {}; void inner(const X&) {cout << "inner(const X&)" << endl;} void inner(X&&) {cout << "inner(X&&)" << endl;} template<typename T> void outer(T&& t) {inner(forward<T>(t));} int main() { X a; outer(a); outer(X()); } //inner(const X&) //inner(X&&)
以上是“C++11中右值引用和轉發型引用的示例分析”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。