您好,登錄后才能下訂單哦!
這篇文章主要介紹c++11新標準中移動語義與右值引用是什么,文中介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們一定要看完!
1.移動語義
C++11新標準中一個最主要的特性就是提供了移動而非拷貝對象的能力。如此做的好處就是,在某些情況下,對象拷貝后就立即被銷毀了,此時如果移動而非拷貝對象會大幅提升性能。參考如下程序:
//moveobj.cpp #include <iostream> #include <vector> using namespace std; class Obj { public: Obj(){cout <<"create obj" << endl;} Obj(const Obj& other){cout<<"copy create obj"<<endl;} }; vector<Obj> foo() { vector<Obj> c; c.push_back(Obj()); cout<<"---- exit foo ----"<<endl; return c; } int main() { vector<Obj> v; v=foo(); }
編譯并運行:
[b3335@localhost test]$ g++ moveobj.cpp
[b3335@localhost test]$ ./a.out
create obj
copy create obj
---- exit foo ----
copy create obj
可見,對obj對象執行了兩次拷貝構造。vector是一個常用的容器了,我們可以很容易的分析這這兩次拷貝構造的時機:
(1)第一次是在函數foo中通過臨時Obj的對象Obj()構造一個Obj對象并入vector中;
(2)第二次是通過從函數foo中返回的臨時的vector對象來給v賦值時發生了元素的拷貝。
由于對象的拷貝構造的開銷是非常大的,因此我們想就可能避免他們。其中,第一次拷貝構造是vector的特性所決定的,不可避免。但第二次拷貝構造,在C++ 11中就是可以避免的了。
[b3335@localhost test]$ g++ -std=c++11 moveobj.cpp
[b3335@localhost test]$ ./a.out
create obj
copy create obj
---- exit foo ----
可以看到,我們除了加上了一個-std=c++11選項外,什么都沒干,但現在就把第二次的拷貝構造給去掉了。它是如何實現這一過程的呢?
在老版本中,當我們執行第二行的賦值操作的時候,執行過程如下:
(1)foo()函數返回一個臨時對象(這里用tmp來標識它);
(2)執行vector的 ‘=' 函數,將對象v中的現有成員刪除,將tmp的成員復制到v中來;
(3)刪除臨時對象tmp。
在C++11的版本中,執行過程如下:
(1)foo()函數返回一個臨時對象(這里用tmp來標識它);
(2)執行vector的 ‘=' 函數,釋放對象v中的成員,并將tmp的成員移動到v中,此時v中的成員就被替換成了tmp中的成員;
(3)刪除臨時對象tmp。
關鍵的過程就是第2步,它是移動而不是復制,從而避免了成員的拷貝,但效果卻是一樣的。不用修改代碼,性能卻得到了提升,對于程序員來說就是一份免費的午餐。但是,這份免費的午餐也不是無條件就可以獲取的,需要帶上-std=c++11來編譯。
2.右值引用
2.1右值引用簡介
為了支持移動操作,C++11引入了一種新的引用類型——右值引用(rvalue reference)。所謂的右值引用指的是必須綁定到右值的引用。使用&&來獲取右值引用。這里給右值下個定義:只能出現在賦值運算符右邊的表達式才是右值。相應的,能夠出現在賦值運算符左邊的表達式就是左值,注意,左值也可以出現在賦值運算符的右邊。對于常規引用,為了與右值引用區別開來,我們可以稱之為左值引用(lvalue reference)。下面是左值引用與右值引用示例:
int i=42; int& r=i; //正確,左值引用 int&& rr=i; //錯誤,不能將右值引用綁定到一個左值上 int& r2=i*42; //錯誤,i*42是一個右值 const int& r3=i*42; //正確:可以將一個const的引用綁定到一個右值上 int&& rr2=i*42; //正確:將rr2綁定到乘法結果上
從上面可以看到左值與右值的區別有:
(1)左值一般是可尋址的變量,右值一般是不可尋址的字面常量或者是在表達式求值過程中創建的可尋址的無名臨時對象;
(2)左值具有持久性,右值具有短暫性。
不可尋址的字面常量一般會事先生成一個無名臨時對象,再對其建立右值引用。所以右值引用一般綁定到無名臨時對象,無名臨時對象具有如下兩個特性:
(1)臨時對象將要被銷毀;
(2)臨時對象無其他用戶。
這兩個特性意味著,使用右值引用的代碼可以自由地接管所引用的對象的資源。
2.2 std::move 強制轉化為右值引用
雖然不能直接對左值建立右值引用,但是我們可以顯示地將一個左值轉換為對應的右值引用類型。我們可以通過調用C++11在標準庫中<utility>中提供的模板函數std::move來獲得綁定到左值的右值引用。示例如下:
int&& rr1=42; int&& rr2=rr1; //error,表達式rr1是左值 int&& rr2=std::move(rr1); //ok
上面的代碼說明了右值引用也是左值,不能對右值引用建立右值引用。move告訴編譯器,在對一個左值建立右值引用后,除了對左值進行銷毀和重新賦值,不能夠再訪問它。std::move在VC10.0版本的STL庫中定義如下:
/* * @brief Convert a value to an rvalue. * @param __t A thing of arbitrary type. * @return The parameter cast to an rvalue-reference to allow moving it. */ template<typename _Tp> constexpr typename std::remove_reference<_Tp>::type&& move(_Tp&& __t) noexcept{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); } template<class _Ty> struct remove_reference{ // remove reference typedef _Ty type; }; template<class _Ty> struct remove_reference<_Ty&>{ // remove reference typedef _Ty type; }; template<class _Ty> struct remove_reference<_Ty&&>{ // remove rvalue reference typedef _Ty type; };
move的參數是接收一個任意類型的右值引用,通過引用折疊,此參數可以與任意類型實參匹配。特別的,我們既可以傳遞左值,也可以傳遞右值給std::move:
string s1("hi"); string&& s2=std::move(string("bye")); //正確:從一個右值移動數據 string&& s3=std::move(s1); //正確:在賦值之后,s1的值是不確定的
注意:
(1)std::move函數名稱具有一定迷惑性,實際上std::move并沒有移動任何東西,本質上就是一個static_cast<T&&>,它唯一的功能是將一個左值強制轉化為右值引用,進而可以使用右值引用使用該值,以用于移動語義。
(2)typename為什么會出現在std::move返回值前面?這里需要明白typename的兩個作用,一個是申明模板中的類型參數,二是在模板中標明“內嵌依賴類型名”(nested dependent type name)[3]^{[3]}[3]。“內嵌依賴類型名”中“內嵌”是指類型定義在類中。以上type是定義在struct remove_reference;“依賴”是指依賴于一個模板參數,上面的std::remove_reference<_Tp>::type&&依賴模板參數_Tp。“類型名”是指這里最終要使用的是個類型名,而不是變量。
2.3 std::forward實現完美轉發
完美轉發(perfect forwarding)指在函數模板中,完全依照模板參數的類型,將參數傳遞給函數模板中調用的另外一個函數,如:
template<typename T> void IamForwording(T t) { IrunCodeActually(t); }
其中,IamForwording是一個轉發函數模板,函數IrunCodeActually則是真正執行代碼的目標函數。對于目標函數IrunCodeActually而言,它總是希望獲取的參數類型是傳入IamForwording時的參數類型。這似乎是一件簡單的事情,實際并非如此。為何還要進行完美轉發呢?因為右值引用本身是個左值,當一個右值引用類型作為函數的形參,在函數內部再轉發該參數的時候它實際上是一個左值,并不是它原來的右值引用類型了。考察如下程序:
template<typename T> void PrintT(T& t) { cout << "lvalue" << endl; } template<typename T> void PrintT(T && t) { cout << "rvalue" << endl; } template<typename T> void TestForward(T&& v) { PrintT(v); } int main() { TestForward(1); //輸出lvaue,理應輸出rvalue }
實際上,我們只需要使用函數模板std::forward即可完成完美轉發,按照參數本來的類型轉發出去,考察如下程序:
template<typename T> void TestForward(T&& v) { PrintT(std::forward<T>(v)); } int main() { TestForward(1); //輸出rvalue int x=1; TestForward(x); //輸出lvalue }
下面給出std::forward的簡單實現:
template<typename T> struct RemoveReference { typedef T Type; }; template<typename T> struct RemoveReference<T&> { typedef T Type; }; template<typename T> struct RemoveReference<T&&> { typedef T Type; }; template<typename T> constexpr T&& ForwardValue(typename RemoveReference<T>::Type&& value) { return static_cast<T&&>(value); } template<typename T> constexpr T&& ForwardValue(typename RemoveReference<T>::Type& value) { return static_cast<T&&>(value); }
其中函數模板ForwardValue就是對std::forward的簡單實現。
2.4關于引用折疊
C++11中實現完美轉發依靠的是模板類型推導和引用折疊。模板類型推導比較簡單,STL中的容器廣泛使用了類型推導。比如,當轉發函數的實參是類型X的一個左值引用,那么模板參數被推導為X&,當轉發函數的實參是類型X的一個右值引用的話,那么模板的參數被推導為X&&類型。再結合引用折疊規則,就能確定出參數的實際類型。
引用折疊式什么?引用折疊規則就是左值引用與右值引用相互轉化時會發生類型的變化,變化規則為:
1. T& + & => T&
2. T&& + & => T&
3. T& + && => T&
4. T&& + && => T&&
上面的規則中,前者代表接受類型,后者代表進入類型,=>表示引用折疊之后的類型,即最后被推導決斷的類型。簡單總結為:
(1)所有右值引用折疊到右值引用上仍然是一個右值引用;
(2)所有的其他引用類型之間的折疊都將變成左值引用。
通過引用折疊規則保留參數原始類型,完美轉發在不破壞const屬性的前提下,將參數完美轉發到目的函數中。
3.右值引用的作用
右值引用的作用是用于移動構造函數(Move Constructors)和移動賦值運算符( Move Assignment Operator)。為了讓我們自己定義的類型支持移動操作,我們需要為其定義移動構造函數和移動賦值運算符。這兩個成員類似對應的拷貝操作,即拷貝構造和賦值運算符,但它們從給定對象竊取資源而不是拷貝資源。
移動構造函數:
移動構造函數類似于拷貝構造函數,第一個參數是該類類型的一個右值引用,同拷貝構造函數一樣,任何額外的參數都必須有默認實參。完成資源移動后,原對象不再保留資源,但移動構造函數還必須確保原對象處于可銷毀的狀態。
移動構造函數的相對于拷貝構造函數的優點:移動構造函數不會因拷貝資源而分配內存,僅僅接管源對象的資源,提高了效率。
移動賦值運算符:
移動賦值運算符類似于賦值運算符,進行的是資源的移動操作而不是拷貝操作從而提高了程序的性能,其接收的參數也是一個類對象的右值引用。移動賦值運算符必須正確處理自賦值。
下面給出移動構造函數和移動析構函數利用右值引用來提升程序效率的實例,首先我先寫了一個山寨的vector:
#include <iostream> #include <string> using namespace std; class Obj { public: Obj(){cout <<"create obj" << endl;} Obj(const Obj& other){cout<<"copy create obj"<<endl;} }; template <class T> class Container { public: T* value; public: Container() : value(NULL) {}; ~Container() { if(value) delete value; } //拷貝構造函數 Container(const Container& other) { value = new T(*other.value); cout<<"in constructor"<<endl; } //移動構造函數 Container(Container&& other) { if(value!=other.value){ value = other.value; other.value = NULL; } cout<<"in move constructor"<<endl; } //賦值運算符 const Container& operator = (const Container& rhs) { if(value!=rhs.value) { delete value; value = new T(*rhs.value); } cout<<"in assignment operator"<<endl; return *this; } //移動賦值運算符 const Container& operator = (Container&& rhs) { if(value!=rhs.value) { delete value; value=rhs.value; rhs.value=NULL; } cout<<"in move assignment operator"<<endl; return *this; } void push_back(const T& item) { delete value; value = new T(item); } }; Container<Obj> foo() { Container<Obj> c; c.push_back(Obj()); cout << "---- exit foo ----" << endl; return c; } int main() { Container<Obj> v; v=foo(); //采用移動構造函數來構造臨時對象,再將臨時對象采用移動賦值運算符移交給v getchar(); }
程序輸出:
create obj
copy create obj
---- exit foo ----
in move constructor
in move assignment operator
上面構造的容器只能存放一個元素,但是不妨礙演示。從函數foo中返回容器對象全程采用移動構造函數和移動賦值運算符,所以沒有出現元素的拷貝情況,提高了程序效率。如果去掉Container的移動構造函數和移動賦值運算符,程序結果如下:
create obj
copy create obj
---- exit foo ----
copy create obj
in constructor
copy create obj
in assignment operator
可見在構造容器Container的臨時對象tmp時發生了元素的拷貝,然后由臨時對象tmp再賦值給v時,又發生了一次元素的拷貝,結果出現了無謂的兩次元素拷貝,這嚴重降低了程序的性能。由此可見,右值引用通過移動構造函數和移動賦值運算符來實現對象移動在C++程序開發中的重要性。
同理,如果想以左值來調用移動構造函數構造容器Container的話,那么需要將左值對象通過std::move來獲取對其的右值引用,參考如下代碼:
//緊接上面的main函數中的內容 Container<Obj> c=v; //調用普通拷貝構造函數,發生元素拷貝 cout<<"-------------------"<<endl; Container<Obj> c1=std::move(v); //獲取對v的右值引用,然后調用移動構造函數構造c1 cout<<c1.value<<endl; cout<<v.value<<endl; //v的元素值已經在動構造函數中被置空(被移除)
代碼輸出:
copy create obj
in constructor
-------------------
in move constructor
00109598
00000000
以上是c++11新標準中移動語義與右值引用是什么的所有內容,感謝各位的閱讀!希望分享的內容對大家有幫助,更多相關知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。