1. 程式人生 > 程式設計 >深入瞭解c++11 移動語義與右值引用

深入瞭解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 移動語義與右值引用的詳細內容,更多關於c++11 移動語義與右值引用的資料請關注我們其它相關文章!