右值涉及到臨時(shí)對(duì)象-就像doubleValues()返回值。如果我們非常清楚的知道從一個(gè)表達(dá)式返回的值是臨時(shí)的,并知道如何編寫(xiě)重載臨時(shí)對(duì)象的方法,這不是很好么?為什么,事實(shí)的確如此。什么是右值引用,就是綁定到臨時(shí)對(duì)象的引用!
在C++11之前,如果有一個(gè)臨時(shí)對(duì)象,就需要使用“正式(regular)”或者“左值引用(lvalue reference)”來(lái)綁定,但如果該值是const呢?如:
1 const string& name = getName(); // ok 2 string& name = getName(); // NOT ok
顯而易見(jiàn)這里不能使用一個(gè)“可變(mutable)”引用,因?yàn)槿绻@么做了,你將可以修改即將銷(xiāo)毀的對(duì)象,這是相當(dāng)危險(xiǎn)的。順便提醒一下,將臨時(shí)對(duì)象保存在const引用中可以確保該臨時(shí)對(duì)象不會(huì)被立刻銷(xiāo)毀。這一個(gè)好的C++編程習(xí)慣,但是它仍然是一個(gè)臨時(shí)對(duì)象,不能夠被修改。
然而在C++11中,引進(jìn)了一種新的引用,即“右值引用”,允許綁定一個(gè)可變引用到一個(gè)右值,不是左值。換句話說(shuō),右值引用專(zhuān)注于檢測(cè)一個(gè)值是否為臨時(shí)對(duì)象。右值使用&&語(yǔ)法而不是&,可以是const和非const的,就像左值引用一樣,盡管你很少看到const左值引用。
1 const string&& name = getName(); // ok 2 string&& name = getName(); // also ok - praise be!
到目前為止一切都運(yùn)行良好,但這是如何實(shí)現(xiàn)的?左值引用和右值引用最重要的區(qū)別,是用著函數(shù)參數(shù)的左值和右值。看看如下兩個(gè)函數(shù):
1 printReference (const String& str) 2 { 3 cout << str; 4 } 5 6 printReference (String&& str) 7 { 8 cout << str; 9 }
這里函數(shù)printReference()的行為就有意思了:printReference (const String& str)接受任何參數(shù),左值和右值都可以,不管左值或右值是否為可變。printReference (String&& str)接受除可變右值引用的任何參數(shù)。換句話說(shuō),如下寫(xiě):
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
現(xiàn)在我們應(yīng)該有一種方法來(lái)確定是否對(duì)臨時(shí)對(duì)象或非臨時(shí)對(duì)象使用引用。右值引用版本的方法就像進(jìn)入俱樂(lè)部(無(wú)聊的俱樂(lè)部,我猜的)的秘密后門(mén),如果是臨時(shí)對(duì)象,則只能進(jìn)。既然我們有方法確定一個(gè)對(duì)象是否為臨時(shí)對(duì)象,哪我們?cè)撊绾问褂媚兀?/p>
move構(gòu)造函數(shù)和move賦值操作符
當(dāng)你使用右值引用時(shí),最常見(jiàn)的模式是創(chuàng)建move構(gòu)造函數(shù)和move賦值操作符(遵循相同的原則)。move構(gòu)造函數(shù),跟拷貝構(gòu)造函數(shù)一樣,以一個(gè)實(shí)例對(duì)象作為參數(shù)創(chuàng)建一個(gè)新的基于原始實(shí)例對(duì)象的實(shí)例。然后move構(gòu)造函數(shù)可以避免內(nèi)存分配,因?yàn)槲覀冎浪呀?jīng)提供了一個(gè)臨時(shí)對(duì)象,而不是復(fù)制整個(gè)對(duì)象,只是“移動(dòng)”而已。假如我們有一個(gè)簡(jiǎn)單的ArrayWrapper類(lèi),如下:
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 };
注意,這里的復(fù)制拷貝構(gòu)造函數(shù)每次都會(huì)分配內(nèi)存和復(fù)制數(shù)組中的每個(gè)元素。對(duì)于復(fù)制操作是如此龐大的工作量,讓我們來(lái)添加move拷貝構(gòu)造函數(shù),獲得高效的性能。
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 };
實(shí)際上move構(gòu)造函數(shù)比copy構(gòu)造函數(shù)更簡(jiǎn)單,這是相當(dāng)不錯(cuò)的。主要注意以下兩點(diǎn):
1、參數(shù)是非const的右值引用
2、other._p_vals應(yīng)置為NULL
以上的第2點(diǎn)是對(duì)第1點(diǎn)的解釋?zhuān)慈绻覀兪褂胏onst右值引用,則不能將other._p_vals置為NULL。但為什么要將other._p_vals置為NULL呢?原因在于析構(gòu)函數(shù),當(dāng)臨時(shí)對(duì)象離開(kāi)其作用域,就像所有其他C++對(duì)象一樣,它們的析構(gòu)函數(shù)都會(huì)被調(diào)用。當(dāng)析構(gòu)函數(shù)被調(diào)用后, _p_vals將被釋放。這里我們只是復(fù)制了_p_vals,如果我們不將_p_vals置為NULL,move就不是真正的“移動(dòng)”,而是復(fù)制,一旦我們使用已釋放的內(nèi)存就會(huì)引發(fā)運(yùn)行奔潰。move構(gòu)造函數(shù)的意義在于,通過(guò)改變?cè)嫉呐R時(shí)對(duì)象來(lái)避免復(fù)制操作。
再次重復(fù),重載move構(gòu)造函數(shù)是為了僅當(dāng)為臨時(shí)對(duì)象時(shí)move構(gòu)造函數(shù)才會(huì)被調(diào)用,只有臨時(shí)對(duì)象才能被修改。這意味著,如果函數(shù)的返回值是const對(duì)象,將調(diào)用copy構(gòu)造函數(shù),而不是move構(gòu)造函數(shù),所以不要像這樣寫(xiě):
1 const ArrayWrapper getArrayWrapper (); // makes the move constructor useless, the temporary is const!
有些情況如何在move構(gòu)造函數(shù)中我們還沒(méi)有討論,如類(lèi)中某個(gè)字段也是對(duì)象。觀察如下這個(gè)類(lèi):
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 };
我們的數(shù)組有字段name和size,因此我們應(yīng)該改變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 };
這樣就可以了??jī)H僅在ArrayWrapper中調(diào)用MetaData的move構(gòu)造函數(shù)就可以了,一切都很自然,不是么?問(wèn)題在于這樣做是不行的!原因很簡(jiǎn)單:move構(gòu)造函數(shù)中的other是右值引用。這里應(yīng)該是右值,而不是右值引用!如果是左值,則調(diào)用copy構(gòu)造函數(shù),而不是move構(gòu)造函數(shù)。有些奇怪,有點(diǎn)繞,對(duì)吧-我知道。這里有種方法可以區(qū)分:右值就是一個(gè)創(chuàng)建稍后會(huì)被銷(xiāo)毀的表達(dá)式。臨時(shí)對(duì)象即將被銷(xiāo)毀時(shí),我們將其傳入move構(gòu)造函數(shù)中,就相當(dāng)于給了它第二次生命,在新的作用域仍然有效。文中右值出現(xiàn)的地方,都是這么做的。在我們的構(gòu)造函數(shù)里,對(duì)象有一個(gè)name字段,它在函數(shù)內(nèi)部一直有效。換句話說(shuō),我們可以在函數(shù)中使用它多次,函數(shù)內(nèi)部定義的臨時(shí)變量在該函數(shù)內(nèi)部一直有效。左值是可以被定位的,我們可以在內(nèi)存某個(gè)位置訪問(wèn)一個(gè)左值。實(shí)際上,在函數(shù)中我們可能想稍后再使用它。如果move構(gòu)造被調(diào)用,這時(shí)我們就有一個(gè)右值引用對(duì)象,就可以使用“移動(dòng)的”對(duì)象了。
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 }
最后一種情況:左值和右值引用都是左值表達(dá)式。不用之處在于,左值引用必須是const綁定到右值,然而右值引用總是可以綁定一個(gè)引用到右值上。類(lèi)似于指針和指針?biāo)赶虻膬?nèi)容的區(qū)別。使用的值來(lái)至于右值,但是當(dāng)我們使用右值本身時(shí),它又成為左值。
std::move
那么有什么技巧可以處理這樣的情況?我們可以使用std::move,包含在<utility>中。如果你想將左值轉(zhuǎn)換為右值,可以使用std::move,這里std::move本身并不移動(dòng)任何東西,它只是將左值轉(zhuǎn)換成右值而已,也可以調(diào)用move構(gòu)造函數(shù)來(lái)實(shí)現(xiàn)。請(qǐng)看如下代碼:
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 }
同樣的,也應(yīng)該修改MetaData:
1 MetaData (MetaData&& other) 2 : _name( std::move( other._name ) ) // oh, blissful efficiency 3 : _size( other._size ) 4 {}
賦值操作符
如同move構(gòu)造函數(shù)一樣,我們也應(yīng)該有一個(gè)move賦值操作符,編寫(xiě)方式跟move構(gòu)造函數(shù)一樣。
Move構(gòu)造函數(shù)和隱式構(gòu)造函數(shù)
正如你所知道的,在C++中只要你手動(dòng)聲明了構(gòu)造函數(shù),編譯器就不會(huì)再為你產(chǎn)生默認(rèn)的構(gòu)造函數(shù)了。這里也是如此:為類(lèi)添加move構(gòu)造函數(shù)要求你定義和聲明一個(gè)默認(rèn)構(gòu)造函數(shù)。另外,聲明move構(gòu)造函數(shù)并不會(huì)阻止編譯器為你產(chǎn)生隱式的copy構(gòu)造函數(shù),聲明move賦值操作符也不會(huì)阻止編譯器創(chuàng)建標(biāo)準(zhǔn)的賦值操作符。
std::move是如何工作的
你或許會(huì)疑惑:如何編寫(xiě)一個(gè)類(lèi)似與std::move這樣的函數(shù)?右值引用轉(zhuǎn)換為左值引用是如何實(shí)現(xiàn)的?可能你已經(jīng)猜到答案了,就是typecasting。std::move的實(shí)際聲明比較復(fù)雜,但其核心思想就是static_cast到右值引用。這就意味著,實(shí)際上你并不真的需要使用move——但你應(yīng)該這樣做,這樣能夠更清楚表達(dá)你的意思。實(shí)際上轉(zhuǎn)換是必要,是件好事,這樣可以防止你意外地將左值轉(zhuǎn)換為右值,因?yàn)槟菢訉?dǎo)致意外的move發(fā)生,是相當(dāng)危險(xiǎn)的。你必須顯示地使用std::move(或者一個(gè)轉(zhuǎn)換)將左值轉(zhuǎn)換為右值引用,右值引用不會(huì)綁定它自己的左值上。
函數(shù)返回顯式的右值引用
什么時(shí)候時(shí)候你應(yīng)該寫(xiě)一個(gè)返回一個(gè)右值引用的函數(shù)?函數(shù)返回右值引用意味著什么呢?通過(guò)值返回對(duì)象的函數(shù)是不是就已經(jīng)是右值了?
我們先回答第二個(gè)問(wèn)題:返回顯式的右值引用與通過(guò)值(by value)返回對(duì)象是不同的。讓我們看看下面的例子:
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 }
明顯在第一種情況里,盡管事實(shí)上getInt()是右值,但這里仍然對(duì)x執(zhí)行了copy操作。我們可以寫(xiě)個(gè)輔助函數(shù),看看:
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 );
運(yùn)行發(fā)現(xiàn),二者打印的x地址明顯不同。另一方面:
1 printAddress( getRvalueInt() ); 2 printAddress( x );
打印的x地址是相同的,這是因?yàn)間etRvalueInt()顯式的返回了一個(gè)右值。
所以返回右值引用與不返回右值引用明顯是不同的。如果你返回已經(jīng)存在的對(duì)象,而不是在函數(shù)內(nèi)部創(chuàng)建的臨時(shí)對(duì)象(編譯器可能會(huì)為你做返回值優(yōu)化,避免copy操作)時(shí),這種不同表現(xiàn)得最為明顯。
現(xiàn)在的問(wèn)題是,你是否需要這么做。答案是:很可能不會(huì)。大多數(shù)情況下,你最有可能得到一個(gè)懸空的(dangling)右值(一種情況是:引用存在,但它引用的臨時(shí)對(duì)象已經(jīng)銷(xiāo)毀了)。這種情況的危險(xiǎn)程度類(lèi)似于被引用的對(duì)象已經(jīng)不存在的左值。右值引用并不總是可以保證對(duì)象有效。返回右值引用主要使這種特殊情況有意義:你有一個(gè)成員函數(shù),該函數(shù)通過(guò)std::move返回類(lèi)中的字段。
Move語(yǔ)義和標(biāo)準(zhǔn)庫(kù)
回到最開(kāi)始的例子中,我們正在使用vector,但未控制類(lèi)vector,也不知道vector是否move構(gòu)造函數(shù)或move賦值操作符。幸運(yùn)地是標(biāo)準(zhǔn)委員會(huì)已經(jīng)將move語(yǔ)義添加到了標(biāo)準(zhǔn)庫(kù)中,這意味著你可以高效地返回vectors, maps, strings,以及你想要返回的任何標(biāo)準(zhǔn)庫(kù)對(duì)象,充分利用move語(yǔ)義吧。
STL容器中可移動(dòng)的對(duì)象
事實(shí)上,標(biāo)準(zhǔn)庫(kù)做得更加地好了。如果你在你的對(duì)象中通過(guò)創(chuàng)建move構(gòu)造函數(shù)和move賦值操作符來(lái)使用move語(yǔ)義,當(dāng)你將這些對(duì)象存儲(chǔ)在STL容器中,STL將自動(dòng)使用std::move,充分利用move語(yǔ)義為你避免效率底下的copy操作。