右值涉及到臨時對象-就像doubleValues()返回值。如果我們非常清楚的知道從一個表達式返回的值是臨時的,并知道如何編寫重載臨時對象的方法,這不是很好么?為什么,事實的確如此。什么是右值引用,就是綁定到臨時對象的引用!
在C++11之前,如果有一個臨時對象,就需要使用“正式(regular)”或者“左值引用(lvalue reference)”來綁定,但如果該值是const呢?如:
1 const string& name = getName(); // ok 2 string& name = getName(); // NOT ok
顯而易見這里不能使用一個“可變(mutable)”引用,因為如果這么做了,你將可以修改即將銷毀的對象,這是相當危險的。順便提醒一下,將臨時對象保存在const引用中可以確保該臨時對象不會被立刻銷毀。這一個好的C++編程習慣,但是它仍然是一個臨時對象,不能夠被修改。
然而在C++11中,引進了一種新的引用,即“右值引用”,允許綁定一個可變引用到一個右值,不是左值。換句話說,右值引用專注于檢測一個值是否為臨時對象。右值使用&&語法而不是&,可以是const和非const的,就像左值引用一樣,盡管你很少看到const左值引用。
1 const string&& name = getName(); // ok 2 string&& name = getName(); // also ok - praise be!
到目前為止一切都運行良好,但這是如何實現的?左值引用和右值引用最重要的區別,是用著函數參數的左值和右值。看看如下兩個函數:
1 printReference (const String& str) 2 { 3 cout << str; 4 } 5 6 printReference (String&& str) 7 { 8 cout << str; 9 }
這里函數printReference()的行為就有意思了:printReference (const String& str)接受任何參數,左值和右值都可以,不管左值或右值是否為可變。printReference (String&& str)接受除可變右值引用的任何參數。換句話說,如下寫:
1 string me( "alex" ); 2 printReference( me ); // calls the first printReference function, taking an lvalue reference 3 printReference( getName() ); // calls the second printReference function, taking a mutable rvalue reference
現在我們應該有一種方法來確定是否對臨時對象或非臨時對象使用引用。右值引用版本的方法就像進入俱樂部(無聊的俱樂部,我猜的)的秘密后門,如果是臨時對象,則只能進。既然我們有方法確定一個對象是否為臨時對象,哪我們該如何使用呢?
move構造函數和move賦值操作符
當你使用右值引用時,最常見的模式是創建move構造函數和move賦值操作符(遵循相同的原則)。move構造函數,跟拷貝構造函數一樣,以一個實例對象作為參數創建一個新的基于原始實例對象的實例。然后move構造函數可以避免內存分配,因為我們知道它已經提供了一個臨時對象,而不是復制整個對象,只是“移動”而已。假如我們有一個簡單的ArrayWrapper類,如下:
1 class ArrayWrapper 2 { 3 public: 4 ArrayWrapper (int n) 5 : _p_vals( new int[ n ] ) 6 , _size( n ) 7 {} 8 // copy constructor 9 ArrayWrapper (const ArrayWrapper& other) 10 : _p_vals( new int[ other._size ] ) 11 , _size( other._size ) 12 { 13 for ( int i = 0; i < _size; ++i ) 14 { 15 _p_vals[ i ] = other._p_vals[ i ]; 16 } 17 } 18 ~ArrayWrapper () 19 { 20 delete [] _p_vals; 21 } 22 private: 23 int *_p_vals; 24 int _size; 25 };
注意,這里的復制拷貝構造函數每次都會分配內存和復制數組中的每個元素。對于復制操作是如此龐大的工作量,讓我們來添加move拷貝構造函數,獲得高效的性能。
1 class ArrayWrapper 2 { 3 public: 4 // default constructor produces a moderately sized array 5 ArrayWrapper () 6 : _p_vals( new int[ 64 ] ) 7 , _size( 64 ) 8 {} 9 10 ArrayWrapper (int n) 11 : _p_vals( new int[ n ] ) 12 , _size( n ) 13 {} 14 15 // move constructor 16 ArrayWrapper (ArrayWrapper&& other) 17 : _p_vals( other._p_vals ) 18 , _size( other._size ) 19 { 20 other._p_vals = NULL; 21 } 22 23 // copy constructor 24 ArrayWrapper (const ArrayWrapper& other) 25 : _p_vals( new int[ other._size ] ) 26 , _size( other._size ) 27 { 28 for ( int i = 0; i < _size; ++i ) 29 { 30 _p_vals[ i ] = other._p_vals[ i ]; 31 } 32 } 33 ~ArrayWrapper () 34 { 35 delete [] _p_vals; 36 } 37 38 private: 39 int *_p_vals; 40 int _size; 41 };
實際上move構造函數比copy構造函數更簡單,這是相當不錯的。主要注意以下兩點:
1、參數是非const的右值引用
2、other._p_vals應置為NULL
以上的第2點是對第1點的解釋,即如果我們使用const右值引用,則不能將other._p_vals置為NULL。但為什么要將other._p_vals置為NULL呢?原因在于析構函數,當臨時對象離開其作用域,就像所有其他C++對象一樣,它們的析構函數都會被調用。當析構函數被調用后, _p_vals將被釋放。這里我們只是復制了_p_vals,如果我們不將_p_vals置為NULL,move就不是真正的“移動”,而是復制,一旦我們使用已釋放的內存就會引發運行奔潰。move構造函數的意義在于,通過改變原始的臨時對象來避免復制操作。
再次重復,重載move構造函數是為了僅當為臨時對象時move構造函數才會被調用,只有臨時對象才能被修改。這意味著,如果函數的返回值是const對象,將調用copy構造函數,而不是move構造函數,所以不要像這樣寫:
1 const ArrayWrapper getArrayWrapper (); // makes the move constructor useless, the temporary is const!
有些情況如何在move構造函數中我們還沒有討論,如類中某個字段也是對象。觀察如下這個類:
1 class MetaData 2 { 3 public: 4 MetaData (int size, const std::string& name) 5 : _name( name ) 6 , _size( size ) 7 {} 8 9 // copy constructor 10 MetaData (const MetaData& other) 11 : _name( other._name ) 12 , _size( other._size ) 13 {} 14 15 // move constructor 16 MetaData (MetaData&& other) 17 : _name( other._name ) 18 , _size( other._size ) 19 {} 20 21 std::string getName () const { return _name; } 22 int getSize () const { return _size; } 23 private: 24 std::string _name; 25 int _size; 26 };
我們的數組有字段name和size,因此我們應該改變ArrayWrapper的定義,如下:
1 class ArrayWrapper 2 { 3 public: 4 // default constructor produces a moderately sized array 5 ArrayWrapper () 6 : _p_vals( new int[ 64 ] ) 7 , _metadata( 64, "ArrayWrapper" ) 8 {} 9 10 ArrayWrapper (int n) 11 : _p_vals( new int[ n ] ) 12 , _metadata( n, "ArrayWrapper" ) 13 {} 14 15 // move constructor 16 ArrayWrapper (ArrayWrapper&& other) 17 : _p_vals( other._p_vals ) 18 , _metadata( other._metadata ) 19 { 20 other._p_vals = NULL; 21 } 22 23 // copy constructor 24 ArrayWrapper (const ArrayWrapper& other) 25 : _p_vals( new int[ other._metadata.getSize() ] ) 26 , _metadata( other._metadata ) 27 { 28 for ( int i = 0; i < _metadata.getSize(); ++i ) 29 { 30 _p_vals[ i ] = other._p_vals[ i ]; 31 } 32 } 33 ~ArrayWrapper () 34 { 35 delete [] _p_vals; 36 } 37 private: 38 int *_p_vals; 39 MetaData _metadata; 40 };
這樣就可以了?僅僅在ArrayWrapper中調用MetaData的move構造函數就可以了,一切都很自然,不是么?問題在于這樣做是不行的!原因很簡單:move構造函數中的other是右值引用。這里應該是右值,而不是右值引用!如果是左值,則調用copy構造函數,而不是move構造函數。有些奇怪,有點繞,對吧-我知道。這里有種方法可以區分:右值就是一個創建稍后會被銷毀的表達式。臨時對象即將被銷毀時,我們將其傳入move構造函數中,就相當于給了它第二次生命,在新的作用域仍然有效。文中右值出現的地方,都是這么做的。在我們的構造函數里,對象有一個name字段,它在函數內部一直有效。換句話說,我們可以在函數中使用它多次,函數內部定義的臨時變量在該函數內部一直有效。左值是可以被定位的,我們可以在內存某個位置訪問一個左值。實際上,在函數中我們可能想稍后再使用它。如果move構造被調用,這時我們就有一個右值引用對象,就可以使用“移動的”對象了。
1 // move constructor 2 ArrayWrapper (ArrayWrapper&& other) 3 : _p_vals( other._p_vals ) 4 , _metadata( other._metadata ) 5 { 6 // if _metadata( other._metadata ) calls the move constructor, using 7 // other._metadata here would be extremely dangerous! 8 other._p_vals = NULL; 9 }
最后一種情況:左值和右值引用都是左值表達式。不用之處在于,左值引用必須是const綁定到右值,然而右值引用總是可以綁定一個引用到右值上。類似于指針和指針所指向的內容的區別。使用的值來至于右值,但是當我們使用右值本身時,它又成為左值。
std::move
那么有什么技巧可以處理這樣的情況?我們可以使用std::move,包含在<utility>中。如果你想將左值轉換為右值,可以使用std::move,這里std::move本身并不移動任何東西,它只是將左值轉換成右值而已,也可以調用move構造函數來實現。請看如下代碼:
1 #include <utility> // for std::move 2 // move constructor 3 ArrayWrapper (ArrayWrapper&& other) 4 : _p_vals( other._p_vals ) 5 , _metadata( std::move( other._metadata ) ) 6 { 7 other._p_vals = NULL; 8 }
同樣的,也應該修改MetaData:
1 MetaData (MetaData&& other) 2 : _name( std::move( other._name ) ) // oh, blissful efficiency 3 : _size( other._size ) 4 {}
賦值操作符
如同move構造函數一樣,我們也應該有一個move賦值操作符,編寫方式跟move構造函數一樣。
Move構造函數和隱式構造函數
正如你所知道的,在C++中只要你手動聲明了構造函數,編譯器就不會再為你產生默認的構造函數了。這里也是如此:為類添加move構造函數要求你定義和聲明一個默認構造函數。另外,聲明move構造函數并不會阻止編譯器為你產生隱式的copy構造函數,聲明move賦值操作符也不會阻止編譯器創建標準的賦值操作符。
std::move是如何工作的
你或許會疑惑:如何編寫一個類似與std::move這樣的函數?右值引用轉換為左值引用是如何實現的?可能你已經猜到答案了,就是typecasting。std::move的實際聲明比較復雜,但其核心思想就是static_cast到右值引用。這就意味著,實際上你并不真的需要使用move——但你應該這樣做,這樣能夠更清楚表達你的意思。實際上轉換是必要,是件好事,這樣可以防止你意外地將左值轉換為右值,因為那樣將導致意外的move發生,是相當危險的。你必須顯示地使用std::move(或者一個轉換)將左值轉換為右值引用,右值引用不會綁定它自己的左值上。
函數返回顯式的右值引用
什么時候時候你應該寫一個返回一個右值引用的函數?函數返回右值引用意味著什么呢?通過值返回對象的函數是不是就已經是右值了?
我們先回答第二個問題:返回顯式的右值引用與通過值(by value)返回對象是不同的。讓我們看看下面的例子:
1 int x; 2 3 int getInt () 4 { 5 return x; 6 } 7 8 int && getRvalueInt () 9 { 10 // notice that it's fine to move a primitive type--remember, std::move is just a cast 11 return std::move( x ); 12 }
明顯在第一種情況里,盡管事實上getInt()是右值,但這里仍然對x執行了copy操作。我們可以寫個輔助函數,看看:
1 void printAddress (const int& v) // const ref to allow binding to rvalues 2 { 3 cout << reinterpret_cast<const void*>( & v ) << endl; 4 } 5 6 printAddress( getInt() ); 7 printAddress( x );
運行發現,二者打印的x地址明顯不同。另一方面:
1 printAddress( getRvalueInt() ); 2 printAddress( x );
打印的x地址是相同的,這是因為getRvalueInt()顯式的返回了一個右值。
所以返回右值引用與不返回右值引用明顯是不同的。如果你返回已經存在的對象,而不是在函數內部創建的臨時對象(編譯器可能會為你做返回值優化,避免copy操作)時,這種不同表現得最為明顯。
現在的問題是,你是否需要這么做。答案是:很可能不會。大多數情況下,你最有可能得到一個懸空的(dangling)右值(一種情況是:引用存在,但它引用的臨時對象已經銷毀了)。這種情況的危險程度類似于被引用的對象已經不存在的左值。右值引用并不總是可以保證對象有效。返回右值引用主要使這種特殊情況有意義:你有一個成員函數,該函數通過std::move返回類中的字段。
Move語義和標準庫
回到最開始的例子中,我們正在使用vector,但未控制類vector,也不知道vector是否move構造函數或move賦值操作符。幸運地是標準委員會已經將move語義添加到了標準庫中,這意味著你可以高效地返回vectors, maps, strings,以及你想要返回的任何標準庫對象,充分利用move語義吧。
STL容器中可移動的對象
事實上,標準庫做得更加地好了。如果你在你的對象中通過創建move構造函數和move賦值操作符來使用move語義,當你將這些對象存儲在STL容器中,STL將自動使用std::move,充分利用move語義為你避免效率底下的copy操作。