C++11新特性:右值引用和轉移構造函數
睿豐德科技 專注RFID識別技術和條碼識別技術與管理軟件的集成項目。質量追溯系統、MES系統、金蝶與條碼系統對接、用友與條碼系統對接
[cpp] view plaincopy
下面的語句就是錯的。
[cpp] view plaincopy
因為左值引用并不是左值,并沒有建立一片穩定的內存空間,所以如果不是const的話你就可以對它的內容進行修改,而右值又不能進行賦值,所以就會出錯。因此只能用const的左值引用來綁定一個右值。 在C++11中,我們可以顯示地使用“右值引用”來綁定一個右值,語法是"&&"。因為指定了是右值引用,所以無論是否const都是正確的。 [cpp] view plaincopy
有了這個功能,我們就可以對原來的左值引用的函數進行重載,重載的函數參數使用右值引用。比如下面這個例子: [cpp] view plaincopy
[cpp] view plaincopy
好了,現在我們知道C++11可以進行顯示的右值引用了。但是我們如果用它來解決一開始那個復制的問題呢? 這就要引入與此相關的另一個新特性,轉移構造函數和轉移賦值運算符
我們可以看到,這個類的拷貝構造函數顯示新建了一片內存空間,然后又對傳進來的左值引用進行了復制。 如果傳進來的實際參數是一個右值(馬上就銷毀),我們自然希望能夠繼續使用這個右值的空間,這樣可以節省申請空間和復制的時間。 我們可以使用轉移構造函數實現這個功能: [cpp] view plaincopy
第一個構造函數就是轉移構造函數。它先將other的域復制給自己。尤其是將_p_vals的指針賦值給自己的指針,這個過程相當于int的復制,所以非常快。然后將other里面_p_vals指針置成NULL。這樣做有什么用呢? 我們看到,這個類的析構函數是這樣的: [cpp] view plaincopy
[cpp] view plaincopy
那么ArrayWrapper類現在就變成這個樣子 [cpp] view plaincopy
同樣,我們使用了轉移構造函數來避免代碼的復制。但是這里的轉移構造函數對嗎? 問題出在下面這條語句 [cpp] view plaincopy
這條語句可以將左值轉換為右值 [cpp] view plaincopy
這樣就可以避免_metadata域的復制了。
感謝Alex Allain提供的部分代碼例子。http://www.cprogramming.com/c++11/rvalue-references-and-move-semantics-in-c++11.html 本文是在他的文章的基礎之上寫出來的。RFID管理系統集成商 RFID中間件 條碼系統中間層 物聯網軟件集成
問題背景
[cpp] view plaincopy
- #include <iostream>
- using namespace std;
- vector<int> doubleValues (const vector<int>& v)
- {
- vector<int> new_values( v.size() );
- for (auto itr = new_values.begin(), end_itr = new_values.end(); itr != end_itr; ++itr )
- {
- new_values.push_back( 2 * *itr );
- }
- return new_values;
- }
- int main()
- {
- vector<int> v;
- for ( int i = 0; i < 100; i++ )
- {
- v.push_back( i );
- }
- v = doubleValues( v );
- }
先來分析一下上述代碼的運行過程。
[cpp] view plaincopy
- vector<int> v;
- for ( int i = 0; i < 100; i++ )
- {
- v.push_back( i );
- }
以上5行語句在棧上新建了一個vector的實例,并在里面放了100個數。
[cpp] view plaincopy
- v = doubleValues( v )
這條語句調用函數doubleValues,函數的參數類型的const reference,常量引用,那么在實參形參結合的時候并不會將v復制一份,而是直接傳遞引用。所以在函數體內部使用的v就是剛才創建的那個vector的實例。
但是
[cpp] view plaincopy
- vector<int> new_values( v.size() );
這條語句新建了一個vector的實例new_values,并且復制了v的所有內容。但這是合理的,因為我們這是要將一個vector中所有的值翻倍,所以我們不應該改變原有的vector的內容。
[cpp] view plaincopy- v = doubleValues( v );
函數執行完之后,new_values中放了翻倍之后的數值,作為函數的返回值返回。但是注意,這個時候doubleValue(v)的調用已經結束。開始執行 = 的語義。
賦值的過程實際上是將返回的vector<int>復制一份放入新的內存空間,然后改變v的地址,讓v指向這篇內存空間。總的來說,我們剛才新建的那個vector又被復制了一遍。
但我們其實希望v能直接得到函數中復制好的那個vector。在C++11之前,我們只能通過傳遞指針來實現這個目的。但是指針用多了非常不爽。我們希望有更簡單的方法。這就是我們為什么要引入右值引用和轉移構造函數的原因。
左值和右值
在說明左值的定義之前,我們可以先看幾個左值的例子。 [cpp] view plaincopy- int a;
- a = 1; // here, a is an lvalue
- int x;
- int& getRef ()
- {
- return x;
- }
- getRef() = 4;
[cpp] view plaincopy
- int x;
- int getVal ()
- {
- return x;
- }
- getVal();
下面的語句就是錯的。
[cpp] view plaincopy
- getVal() = 1;//compilation error
右值引用
在C++11中,你可以使用const的左值引用來綁定一個右值,比如說: [cpp] view plaincopy- const int& val = getVal();//right
- int& val = getVal();//error
因為左值引用并不是左值,并沒有建立一片穩定的內存空間,所以如果不是const的話你就可以對它的內容進行修改,而右值又不能進行賦值,所以就會出錯。因此只能用const的左值引用來綁定一個右值。 在C++11中,我們可以顯示地使用“右值引用”來綁定一個右值,語法是"&&"。因為指定了是右值引用,所以無論是否const都是正確的。 [cpp] view plaincopy
- const string&& name = getName(); // ok
- string&& name = getName(); // also ok
有了這個功能,我們就可以對原來的左值引用的函數進行重載,重載的函數參數使用右值引用。比如下面這個例子: [cpp] view plaincopy
- printReference (const String& str)
- {
- cout << str;
- }
- printReference (String&& str)
- {
- cout << str;
- }
[cpp] view plaincopy
- string me( "alex" );
- printReference( me ); // 調用第一函數,參數為左值常量引用
- printReference( getName() ); 調用第二個函數,參數為右值引用。
好了,現在我們知道C++11可以進行顯示的右值引用了。但是我們如果用它來解決一開始那個復制的問題呢? 這就要引入與此相關的另一個新特性,轉移構造函數和轉移賦值運算符
轉移構造函數和轉移賦值運算符
假設我們定義了一個ArrayWrapper的類,這個類對數組進行了封裝。 [cpp] view plaincopy- class ArrayWrapper
- {
- public:
- ArrayWrapper (int n)
- : _p_vals( new int[ n ] )
- , _size( n )
- {}
- // copy constructor
- ArrayWrapper (const ArrayWrapper& other)
- : _p_vals( new int[ other._size ] )
- , _size( other._size )
- {
- for ( int i = 0; i < _size; ++i )
- {
- _p_vals[ i ] = other._p_vals[ i ];
- }
- }
- ~ArrayWrapper ()
- {
- delete [] _p_vals;
- }
- private:
- int *_p_vals;
- int _size;
- };
我們可以看到,這個類的拷貝構造函數顯示新建了一片內存空間,然后又對傳進來的左值引用進行了復制。 如果傳進來的實際參數是一個右值(馬上就銷毀),我們自然希望能夠繼續使用這個右值的空間,這樣可以節省申請空間和復制的時間。 我們可以使用轉移構造函數實現這個功能: [cpp] view plaincopy
- class ArrayWrapper
- {
- public:
- // default constructor produces a moderately sized array
- ArrayWrapper ()
- : _p_vals( new int[ 64 ] )
- , _size( 64 )
- {}
- ArrayWrapper (int n)
- : _p_vals( new int[ n ] )
- , _size( n )
- {}
- // move constructor
- ArrayWrapper (ArrayWrapper&& other)
- : _p_vals( other._p_vals )
- , _size( other._size )
- {
- other._p_vals = NULL;
- }
- // copy constructor
- ArrayWrapper (const ArrayWrapper& other)
- : _p_vals( new int[ other._size ] )
- , _size( other._size )
- {
- for ( int i = 0; i < _size; ++i )
- {
- _p_vals[ i ] = other._p_vals[ i ];
- }
- }
- ~ArrayWrapper ()
- {
- delete [] _p_vals;
- }
- private:
- int *_p_vals;
- int _size;
- };
第一個構造函數就是轉移構造函數。它先將other的域復制給自己。尤其是將_p_vals的指針賦值給自己的指針,這個過程相當于int的復制,所以非常快。然后將other里面_p_vals指針置成NULL。這樣做有什么用呢? 我們看到,這個類的析構函數是這樣的: [cpp] view plaincopy
- ~ArrayWrapper ()
- {
- delete [] _p_vals;
- }
- ArrayWrapper *aw = new ArrayWrapper((new ArrayWrapper(5)));
- (new ArrayWrapper(5)
[cpp] view plaincopy
- other._p_vals = NULL;
右值引用也是左值
這種說法可能有點繞,來看一個例子: 我們可以定義MetaData類來抽象ArrayWrapper中的數據: [cpp] view plaincopy- class MetaData
- {
- public:
- MetaData (int size, const std::string& name)
- : _name( name )
- , _size( size )
- {}
- // copy constructor
- MetaData (const MetaData& other)
- : _name( other._name )
- , _size( other._size )
- {}
- // move constructor
- MetaData (MetaData&& other)
- : _name( other._name )
- , _size( other._size )
- {}
- std::string getName () const { return _name; }
- int getSize () const { return _size; }
- private:
- std::string _name;
- int _size;
- };
那么ArrayWrapper類現在就變成這個樣子 [cpp] view plaincopy
- class ArrayWrapper
- {
- public:
- // default constructor produces a moderately sized array
- ArrayWrapper ()
- : _p_vals( new int[ 64 ] )
- , _metadata( 64, "ArrayWrapper" )
- {}
- ArrayWrapper (int n)
- : _p_vals( new int[ n ] )
- , _metadata( n, "ArrayWrapper" )
- {}
- // move constructor
- ArrayWrapper (ArrayWrapper&& other)
- : _p_vals( other._p_vals )
- , _metadata( other._metadata )
- {
- other._p_vals = NULL;
- }
- // copy constructor
- ArrayWrapper (const ArrayWrapper& other)
- : _p_vals( new int[ other._metadata.getSize() ] )
- , _metadata( other._metadata )
- {
- for ( int i = 0; i < _metadata.getSize(); ++i )
- {
- _p_vals[ i ] = other._p_vals[ i ];
- }
- }
- ~ArrayWrapper ()
- {
- delete [] _p_vals;
- }
- private:
- int *_p_vals;
- MetaData _metadata;
- };
同樣,我們使用了轉移構造函數來避免代碼的復制。但是這里的轉移構造函數對嗎? 問題出在下面這條語句 [cpp] view plaincopy
- _metadata( other._metadata )
- std::move
這條語句可以將左值轉換為右值 [cpp] view plaincopy
- // 轉移構造函數
- ArrayWrapper (ArrayWrapper&& other)
- : _p_vals( other._p_vals )
- , _metadata( std::move( other._metadata ) )
- {
- other._p_vals = NULL;
- }
這樣就可以避免_metadata域的復制了。
函數返回右值引用
我們可以在函數中顯示地返回一個右值引用 [cpp] view plaincopy- int x;
- int getInt ()
- {
- return x;
- }
- int && getRvalueInt ()
- {
- // notice that it's fine to move a primitive type--remember, std::move is just a cast
- return std::move( x );
- }
感謝Alex Allain提供的部分代碼例子。http://www.cprogramming.com/c++11/rvalue-references-and-move-semantics-in-c++11.html 本文是在他的文章的基礎之上寫出來的。RFID管理系統集成商 RFID中間件 條碼系統中間層 物聯網軟件集成