【譯】VC10中的C++0x特性 Part 2 (1):右值引用
來源:vcblog 作者:Stephan T. Lavavej 翻譯:飄飄白云
(轉載時請注明作者和出處。未經許可,請勿用于商業用途)
簡介
這一系列文章介紹Microsoft Visual Studio 2010 中支持的C++ 0x特性,目前有三部分。
Part 1 :介紹了Lambdas, 賦予新意義的auto,以及 static_assert;
Part 2( 一 , 二 , 三 ):介紹了右值引用(Rvalue References);
Part 3:介紹了表達式類型(decltype)
VC10中的C++0x特性 Part 1,2,3 譯文打包下載(doc 和 pdf 格式): 點此下載
本文為 Part 2 的第一頁
今天我要講的是 rvalue references (右值引用),它能實現兩件不同的事情: move 語意和完美轉發。剛開始會覺得它們難以理解,因為需要區分 lvalues 和 rvalues ,而只有極少數 C++98/03 程序員對此非常熟悉。這篇文章會很長,因為我打算極其詳盡地解釋 rvalue references 的運作機制。
不用害怕,使用 ravlue references 是很容易的,比聽起來要容易得多。要在你的代碼中實現 move semantics 或 perfect forwarding 只需遵循簡單的模式,后文我會對此作演示的。學習如何使用 rvalue references 是絕對值得的,因為 move semantics 能帶來巨大的性能提升,而 perfect forwarding 讓高度泛型代碼的編寫變得非常容易。
C++ 98/03 中的 lvalues 和 rvalues
要理解C++ 0x中的 rvalue references,你得先理解 C++ 98/03 中的 lvalues 與 rvalues。
術語 “lvalues” 和 “rvalues” 是很容易被搞混的,因為它們的歷史淵源也是混淆。(順帶一提,它們的發音是 ‘L values“ 和 ”R values“, 盡管它們都寫成一個單詞)。這兩個概念起初來自 C,后來在 C++ 中被加以發揮。為節省時間,我跳過了有關它們的歷史,比如為什么它們被稱作 “lvalues” 和 “rvalues”,我將直接講它們在 C++ 98/03 中是如何運作的。(好吧,這不是什么大秘密: “L” 代表 “left”,“R” 代表 “right”。它們的含義一直在演化而名字卻沒變,現在已經“名”不副“實”了。與其幫你上一整堂歷史課,不如隨意地把它們當作像“上夸克”和“下夸克”之類的名字,也不會有什么損失。)
C++ 03 標準 3.10/1 節上說: “每一個表達式要么是一個 lvalue ,要么就是一個 rvalue 。” 應該謹記 lvalue 跟 rvalue 是針對表達式而言的,而不是對象。
lvalue 是指那些單一表達式結束之后依然存在的持久對象。例如: obj,*ptr, prt[index], ++x 都是 lvalue。
rvalue 是指那些表達式結束時(在分號處)就不復存在了的臨時對象。例如: 1729 , x + y , std::string("meow") , 和 x++ 都是 rvalue。
注意 ++x 和 x++ 的區別。當我們寫 int x = 0; 時, x 是一個 lvalue,因為它代表一個持久對象。 表達式 ++x 也是一個 lvalue,它修改了 x 的值,但還是代表原來那個持久對象。然而,表達式 x++ 卻是一個 rvalue,它只是拷貝一份持久對象的初值,再修改持久對象的值,最后返回那份拷貝,那份拷貝是臨時對象。 ++x 和 x++ 都遞增了 x,但 ++x 返回持久對象本身,而 x++ 返回臨時拷貝。這就是為什么 ++x 之所以是一個 lvalue,而 x++ 是一個 rvalue。 lvalue 與 rvalue 之分不在于表達式做了什么,而在于表達式代表了什么(持久對象或臨時產物)。
另一個培養判斷一個表達式是不是 lvalue 的直覺感的方法就是自問一下“我能不能對表達式取址?”,如果能夠,那就是一個 lvalue;如果不能,那就是 一個 rvalue。 例如:&obj , &*ptr , &ptr[index] , 和 &++x 都是合法的(即使其中一些例子很蠢),而 &1729 , &(x + y) , &std::string("meow") , 和 &x++ 是不合法的。為什么這個方法湊效?因為取址操作要求它的“操作數必須是一個 lvalue”(見 C++ 03 5.3.1/2)。為什么要有那樣的規定?因為對一個持久對象取址是沒問題的,但對一個臨時對象取址是極端危險的,因為臨時對象很快就會被銷毀(譯注:就像你有一個指向某個對象的指針,那個對象被釋放了,但你還在使用那個指針,鬼知道這時候指針指向的是什么東西)。
前面的例子不考慮操作符重載的情況,它只是普通的函數調用語義。“一個函數調用是一個 lvalue 當且僅當它返回一個引用”(見 C++ 03 5.2.2/10)。因此,給定語句 vercor<int> v(10, 1729); , v[0] 是一個 lvalue,因為操作符 []() 返回 int& (且 &v[0] 是合法可用的); 而給定語句 string s("foo");和 string t("bar");,s + t 是一個rvalue,因為操作符 +() 返回 string(而 &(s + t) 也不合法)。
lvalue 和 rvalue 兩者都有非常量(modifiable,也就是說non-const)與常量(const )之分。舉例來說:
string one("cute");
const string two("fluffy");
string three() { return "kittens"; }
const string four() { return "are an essential part of a healthy diet"; }
one; // modifiable lvalue
two; // const lvalue
three(); // modifiable rvalue
four(); // const rvalue
Type& 可綁定到非常量 lvalue (可以用這個引用來讀取和修改原來的值),但不能綁定到 const lvalue,因為那將違背 const 正確性;也不能把它綁定到非常量 rvalue,這樣做極端危險,你用這個引用來修改臨時對象,但臨時對象早就不存在了,這將導致難以捕捉而令人討厭的 bug,因此 C++ 明智地禁止這這么做。(我要補充一句:VC 有一個邪惡的擴展允許這么蠻干,但如果你編譯的時候加上參數 /W4 ,編譯器通常會提示警告"邪惡的擴展被激活了”)。也不能把它綁定到 const ravlue,因為那會是雙倍的糟糕。(細心的讀者應該注意到了我在這里并沒有談及模板參數推導)。
const Type& 可以綁定到: 非常量 lvalues, const lvalues,非常量 rvalues 以及 const values。(然后你就可以用這個引用來觀察它們)
引用是具名的,因此一個綁定到 rvalue 的引用,它本身是一個 lvalue(沒錯!是 L)。(因為只有 const 引用可以綁定到 rvalue,所以它是一個 const lvalue)。這讓人費解,(不弄清楚的話)到后面會更難以理解,因此我將進一步解釋。給定函數 void observe(const string& str), 在 observe()'s 的實現中, str 是一個 const lvalue,在 observe() 返回之前可以對它取址并使用那個地址。這一點即使我們通過傳一個 rvalue 參數來調用 observe()也是成立的 ,就像上面的 three() 和 four()。也可以調用 observe("purr"),它構建一個臨時 string 并將 str 綁定到那個臨時 string。three() 和 foure() 的返回對象是不具名的,因此他們是 rvalue,但是在 observe()中,str 是具名的,所以它是一個 lvalue。正如前面我說的“ lvalue 跟 rvalue 是針對表達式而言的,而不是對象”。當然,因為 str 可以被綁定到一個很快會被銷毀的臨時對象,所以在 observe() 返回之后我們就不應該在任何地方保存這個臨時對象的地址。
你有沒有對一個綁定到 rvalue 的 const 引用取址過么?當然,你有過!每當你寫一個帶自賦值檢查的拷貝賦值操作符: Foo& operator=(const Foo& other), if( this != &other) { copy struff;}; 或從一個臨時變量來拷貝賦值,像: Foo make_foo(); Foo f; f = make_foo(); 的時候,你就做了這樣的事情。
這個時候,你可能會問“那么非常量 rvalues 跟 const rvalues 有什么不同呢?我不能將 Type& 綁定到非常量 rvalue 上,也不能通過賦值等操作來修改 rvalue,那我真的可以修改它們?” 問的很好!在 C++ 98/03 中,這兩者存在一些細微的差異: non-constrvalues 可以調用 non-const 成員函數。 C++ 不希望你意外地修改臨時對象,但直接在non-const rvalues上調用 non-const 成員函數,這樣做是很明顯的,所以這是被允許的。在 C++ 0x中,答案有了顯著的變化,它能用來實現 move 語意。
恭喜!你已經具備了我所謂的“lvalue/rvalue 觀”,這樣你就能夠一眼就判斷出一個表達式到底是 lvalue 還是 rvalue。再加上你原來對 const 的認識,你就能完全理解為什么給定語句 void mutate(string& ref) 以及前面的變量定義, mutate(one) 是合法的,而 mutate(two), mutate(three()), mutate(four()), mutate("purr") 都是不合法的。如果你是 C++ 98/03 程序員,你已經可以分辨出這些調用中的哪些是合法的,哪些是不合法的;是你的“本能直覺”,而不是你的編譯器,告訴你 mutate(three()) 是假冒的。你對 lvalue/rvalue 的新認識讓你明確地理解為什么 three() 是一個 rvalue,也知道為什么非常量引用不能綁定到右值。知道這些有用么?對語言律師而言,有用,但對普通程序員來說并不見得。畢竟,你如果不理解關于 lvalues 和 rvalues 一切就要領悟這個還隔得遠呢。但是重點來了:與 C++ 98/03 相比, C++ 0x 中的 lvalue 和 rvalue 有著更廣泛更強勁的含義(尤其是判斷表達式是否是 modifiable / const 的 lvalue/rvalue,并據此做些處理)。要有效地使用 C++ 0x,你也需具備對 lvalue/rvalue 的理解。現在萬事具備,我們能繼續前行了。
拷貝的問題
C++ 98/03 將不可思議的高度抽象和不可思議的高效執行結合到了一起,但有個問題:它過度濫用拷貝。對行為像 int 那樣有著值語意的對象而言,源對象的拷貝是獨立存在的,并不會影響源對象。值語意很好,除了在會導致冗余拷貝之外,像拷貝 strings,vectors 等重型對象那樣的情況。(“重型”意味著“昂貴的拷貝開銷”;有著100萬個元素的 vector 是重型的)。返回值優化(RVO) 和命名返回值優化(NRVO)在特定情況下可以優化掉拷貝構造操作,這有助于減緩問題的嚴重性,但是它們不能夠消除所有冗余的拷貝。
最最沒有必要的拷貝是拷貝那些立馬會被銷毀的對象。你有過復印一份文件,并馬上把原件扔掉的經歷么(假定原件和復件是相同的)?那簡直是浪費,你應該持有原件而不必費勁去復印。下面是被我稱作“殺手級的示例”,來自標準委員會的例子(見提案 N1377),假設你有一大堆 string 像這樣的:
string s0("my mother told me that");
string s1("cute");
string s2("fluffy");
string s3("kittens");
string s4("are an essential part of a healthy diet");
然后你想像這樣把它們串接起來:
string dest = s0 + " " + s1 + " " + s2 + " " + s3 + " " + s4;
這樣做的效率如何?(我們不用為這個特殊的例子而擔憂,它的執行只要幾微秒;我們擔憂它的一般化情況,在語言層面上的情況)。
每次調用操作符 +() 就會返回一個臨時 string。上面調用了 8 次操作符 +(),因而產生了 8 個臨時 string。 每一個臨時 string,在構造過程中分配動態內存,再拷貝所有已連接的字符,最后在析構過程中釋放分配的動態內存。(你聽說過短串優化技術么,為了避免動態內存的分配與釋放,VC是這么干的,在這個被我精心挑選的有著合適長度的 s0 面前短串優化技術也無能為力,即使執行了這樣的優化,也無法避免拷貝操作。如果你還聽說過寫時拷貝優化(Copy - On - Write),忘了它吧,在這里也不適用,并且在多線程環境下這種優化會惡化問題,因此標準庫實現根本就不再做這個優化了)。
事實上,因為每一個串接操作都會拷貝所有已經串接好的字符,所以那個復雜度是字符串長度的平方了。哎呀!這太浪費了!這點確實讓 C++ 尷尬。事情怎么會搞成這樣呢?有沒有改善的辦法?
問題是這樣的,operator+()接受兩個參數,一個是 const string&,另一個是 const string& 或 const char * (還有其他重載版本,但在這里我們沒有用到),但 operator+() 無法分辨出你塞給它的是 lvalue 還是 rvalue 參數,所以它只好總是創建一個臨時 string,并返回這個臨時 string。 為什么這跟 vavlue/rvalue 有關系?
當我們要計算 s0 + " " 的值時,很明顯這里有必要創建一個新的臨時 string。 s0 是一個 lvalue,它已經命名了一個持久對象,因此我們不能修改它。(有人注意到了!) 。如果要計算 (s0 + “ ”) + s1 的值,我們可以簡單地將 s1 的內容追加到第一個臨時 string 上,而不用創建第二個臨時 string 再把第一個丟棄掉。這就是 move 語意背后的核心觀念: 因為 s0 + " " 是一個 rvalue ,只有那個在整個程序中唯一能夠覺察到臨時對象存在的表達式可以引用臨時對象。如果我們能檢測到表達式是一個非常量 rvalue,我們就可以任意修改臨時對象,而不會有人發現。 操作符 +() 本不應該修改它的參數,但如果其參數是非常量 rvalue,誰在乎?照這種方法,每次調用操作符 +() 都把字符追加到唯一的臨時對象上,這樣就徹底省掉了不必要的動態內存管理和冗余的拷貝操作,呈現出線性復雜度。耶!
從技術上講,在 C++ 0x 中,每次調用操作符 +() 還是會返回一個單獨的臨時 string。 然而,第二個臨時 string (產生自 (s0 + “ ”) + s1 )可以通過“竊取”第一個臨時 string (產生自 s0 + " " )的內存而被構造出來,然后再把 s1 的內容追加到那塊內存后面(這將會引發一個普通的重分配操作)。“竊取”是通過指針的操作實現的:第二個臨時 string 會先拷貝第一個臨時 string 的內部指針,然后再清空這個指針。第一個臨時 string 最后被銷毀(在分號那地方)時,它的指針已經置為 null 了,因此它的析構函數什么也不會做(譯注:也就是說不會釋放它的內存,這部分內存現在是第二個臨時 string 在使用了)。
通常,如果能夠檢測到非常量 rvalue,你就能夠做些“資源竊取”的優化。如果非常量 rvalue 所引用的那些對象持有任何資源(如內存),你就能竊取它們的資源而不用拷貝它們,反正它們很快就會被銷毀掉。通過竊取非常量 rvalue 持有的資源來構建或賦值的手法通常被稱作 “moving”,可移動對象擁有 “move 語意”。
在大多數情況下這相當有用,比如 vector 的重新分配。當一個 vector 需要更多空間(如 push_back() 時)和進行重分配操作時,它需要從舊的內存塊中拷貝元素到新的內存塊中去。這些拷貝構造調用的開銷很大。(對 vector<string> 來說,需要拷貝每一個 string 元素,這涉及動態內存分配)。但是等一等!舊內存塊中的那些元素很快會被銷毀掉的呀,所以我們可以挪動這些元素,而不用拷貝它們。在這種情形下,舊內存塊中的元素依然存在于內存中,用來訪問它們的表達式,如 old_ptr[index],還是 lvalue。在重分配過程中,我們想用非常量 rvalue 表達式來引用舊內存塊中的元素。假定它們是非常量 rvalue,那我們就能夠移動它們,從而省去拷貝構造開銷。(說”我想假定這個 lvalue 是一個非常量 rvalue “等同于說”我知道這是一個 lvalue,它指向一個持久對象,但我不關心隨后會對這個 lvalue 進行怎樣的操作,或銷毀它,或給它賦值,或進行任意操作。因此如果你能從它那里竊取資源的話,盡管行動吧”)
C++0x 的 rvalue 引用概念給與我們檢測非常量 rvalue 并從中竊取資源的能力,這讓我能夠實現 move 語意。rvalue 引用也讓我們能夠通過把 lvalue 偽裝成非常量 rvalue 而隨意觸發 move 語意。現在,我們來看看 rvalue 引用是如何工作的!
ravlue 引用:初始化
C++0x 引進了一種新的引用,ravlue 引用,其語法是 Type&& 和 const Type&& 。目前 C++0x 草案 N2798 8.3.2/2 上說:“用 & 聲明的引用類型被稱作 lvalue 引用,而用 && 聲明的引用類型被稱作 rvalue 引用。lvalue 引用與 rvalue 引用是截然不同的類型。除非特別注明,兩者在語意上是相當的并且一般都被稱作引用。”這意味著對 C++98/03 中引用(即現在的 lvalue 引用)的直覺印象可以延伸用于 rvalue 引用;你只需要學習這兩者的不同之處。
(說明:我選擇把 Type& 讀作 “Type ref”,Type&& 讀作 "Type ref ref"。它們的全稱分別是 “lvalue reference to Type” 和 "rvalue reference to Type",就像 “cosnt pointer to int” 被寫成 “int * const”,而被讀作 “int star const”一樣。)
兩者有什么區別?與 lvalue 引用相比, rvalue 引用在初始化與重載決議時表現出不同的行為。兩者的區別在于它們會優先綁定到什么東西上(初始化時)和什么東西會優先綁定到它們身上(重載決議時)。首先讓我們來看看初始化:
· 我們已經明白為何非常量 lvalue 引用( Type& ) 只能綁定到非常量 lvalue 上,而其他的一概不能(如 const lvalues,非常量 rvalues,const rvalues)
· 我們已經明白為何 const lvalue 引用( const Type& ) 能綁定到任何東西上。
· 非常量 rvalue ( Type&& ) 能夠綁定到非常量 lvalue 以及非常量 rvalue 上,而不能綁定到 const lvalues 和 const rvalues (這會違背 const 正確性)
· const rvalue 引用( const Type&& ) 能夠綁定到任何東西上。
這些規則聽起來可能有些神秘,但是他們來源于兩條簡單的規則:
· 遵守 const 正確性,所以你不能把非常量引用綁定到常量上。
· 避免意外修改臨時對象,所以你不能把非常量 lvalue 引用綁定到非常量 rvalue 上來。
如果你更喜歡閱讀編譯器錯誤信息,而不是閱讀文字描述,下面是一個示例:
C:\Temp>type initialization.cpp
#include <string>
using namespace std;
string modifiable_rvalue() {
return "cute";
}
const string const_rvalue() {
return "fluffy";
}
int main() {
string modifiable_lvalue("kittens");
const string const_lvalue("hungry hungry zombies");
string& a = modifiable_lvalue; // Line 16
string& b = const_lvalue; // Line 17 - ERROR
string& c = modifiable_rvalue(); // Line 18 - ERROR
string& d = const_rvalue(); // Line 19 - ERROR
const string& e = modifiable_lvalue; // Line 21
const string& f = const_lvalue; // Line 22
const string& g = modifiable_rvalue(); // Line 23
const string& h = const_rvalue(); // Line 24
string&& i = modifiable_lvalue; // Line 26
string&& j = const_lvalue; // Line 27 - ERROR
string&& k = modifiable_rvalue(); // Line 28
string&& l = const_rvalue(); // Line 29 - ERROR
const string&& m = modifiable_lvalue; // Line 31
const string&& n = const_lvalue; // Line 32
const string&& o = modifiable_rvalue(); // Line 33
const string&& p = const_rvalue(); // Line 34
}
C:\Temp>cl /EHsc /nologo /W4 /WX initialization.cpp
initialization.cpp
initialization.cpp(17) : error C2440: 'initializing' : cannot convert from 'const std::string' to 'std::string &'
Conversion loses qualifiers
initialization.cpp(18) : warning C4239: nonstandard extension used : 'initializing' : conversion from 'std::string' to 'std::string &'
A non-const reference may only be bound to an lvalue
initialization.cpp(19) : error C2440: 'initializing' : cannot convert from 'const std::string' to 'std::string &'
Conversion loses qualifiers
initialization.cpp(27) : error C2440: 'initializing' : cannot convert from 'const std::string' to 'std::string &&'
Conversion loses qualifiers
initialization.cpp(29) : error C2440: 'initializing' : cannot convert from 'const std::string' to 'std::string &&'
Conversion loses qualifiers
非常量 rvalue 引用綁定到非常量 rvalue 是沒問題的;要領就是它們可以被用來修改臨時對象。
雖然 lvalue 引用和 rvalue 引用在初始化時有著相似的行為(只有第 18 和 28 行不同),但在重載決議的時候它們的區別就很顯著了。
rvalue 引用:重載決議
函數可根據非常量和常量 lvalue 引用參數的不同而重載,這一點你應該很熟悉了。在 C++0x 中,函數也可根據非常量和常量 rvalue 引用參數的不同而重載。如果給出這四種形式的重載一元函數,你不應為表達式能優先綁定到與之相對應的引用上而決議出相應的重載函數這一點感到驚奇:
C:\Temp>type four_overloads.cpp
#include <iostream>
#include <ostream>
#include <string>
using namespace std;
void meow(string& s) {
cout << "meow(string&): " << s << endl;
}
void meow(const string& s) {
cout << "meow(const string&): " << s << endl;
}
void meow(string&& s) {
cout << "meow(string&&): " << s << endl;
}
void meow(const string&& s) {
cout << "meow(const string&&): " << s << endl;
}
string strange() {
return "strange()";
}
const string charm() {
return "charm()";
}
int main() {
string up("up");
const string down("down");
meow(up);
meow(down);
meow(strange());
meow(charm());
}
C:\Temp>cl /EHsc /nologo /W4 four_overloads.cpp
four_overloads.cpp
C:\Temp>four_overloads
meow(string&): up
meow(const string&): down
meow(string&&): strange()
meow(const string&&): charm()
在實踐中,全部重載 Type& , const Type& , Type&& , const Type&& 并不是很有用。只重載 const Type& 和 Type&& 更有意思些:
C:\Temp>type two_overloads.cpp
#include <iostream>
#include <ostream>
#include <string>
using namespace std;
void purr(const string& s) {
cout << "purr(const string&): " << s << endl;
}
void purr(string&& s) {
cout << "purr(string&&): " << s << endl;
}
string strange() {
return "strange()";
}
const string charm() {
return "charm()";
}
int main() {
string up("up");
const string down("down");
purr(up);
purr(down);
purr(strange());
purr(charm());
}
C:\Temp>cl /EHsc /nologo /W4 two_overloads.cpp
two_overloads.cpp
C:\Temp>two_overloads
purr(const string&): up
purr(const string&): down
purr(string&&): strange()
purr(const string&): charm()
上面的重載決議是怎么作出的呢?下面是規則:
(1) 初始化規則擁有否決權。
(2) lvalue 最優先綁定到 lvalue 引用,rvalue 最優先綁定到 rvalue 引用。
(3) 非常量表達式傾向于綁定到非常量引用上。
(我說的“否決權”是指:進行重載決議時初始化規則否決那些不可行(譯注:不滿足 const 正確性)的候選函數,這些函數阻止將表達式綁定到引用上) 讓我們一條一條來看看這些規則是怎么運作的。
·對 purr(up) 而言,決議(1)初始化規則既不否決 purr(const string&) 也不否決 purr(string&&)。 up 是 lvalue,因此滿足決議(2)中的 lvalue 最優先綁定到 lvalue 引用,即 purr(const string&)。up 還是非常量,因此滿足決議(3)非常量表達式傾向于綁定到非常量引用上,即purr(string&&)。兩者放一塊決議時,決議(2)勝出,選擇 purr(const string&)。
·對 purr(down) 而言, 決議(1)初始化規則基于 const 正確性否決掉 purr(string&&),因此 purr(const string&) 勝出。
·對 purr(strange()) 而言,決議(1)初始化規則既不否決 purr(const string&) 也不否決 purr(string&&)。strange() 是 rvalue, 因此滿足決議(2) rvalue 最優先綁定到 rvalue 引用,即 purr(string&&)。strange() 還是非常量,因此滿足決議(3)非常量表達式傾向于綁定到非常量引用上,即purr(string&&)上。purr(string&&) 在這里兩票勝出。
·對 purr(charm()) 而言,決議(1)初始化規則基于 const 正確性否決掉 purr(string&&),因此 purr(const string&) 勝出。
值得注意的是當你只重載了const Type& 和 Type&& ,非常量 rvalue 綁定到 Type&&,而其它的都綁定到 const Type&。因此,這一組重載用來實現 move 語義。
重要說明:返回值的函數應當返回 Type(如 strange() )而不是返回 const Type (如 charm())。后者不會帶來什么好處(阻止非常量成員函數調用),還會阻止 move 語意優化。
move 語義:模式
下面是一個簡單的類 remote_integer, 內部存儲一個指向動態分配的 int 指針(“遠程擁有權”)。你應該對這個類的默認構造函數,一元構造函數,拷貝構造函數,拷貝賦值函數和析構函數都很熟悉了。我給它增加了 move 構造函數和 move 賦值函數,它們被#ifdef MOVABLE 圍起來了,這樣我就可以演示在有和沒有這兩個函數的情況下會有什么差別,在真實的代碼中是不會這么做的。
C:\Temp>type remote.cpp
#include <stddef.h>
#include <iostream>
#include <ostream>
using namespace std;
class remote_integer {
public:
remote_integer() {
cout << "Default constructor." << endl;
m_p = NULL;
}
explicit remote_integer(const int n) {
cout << "Unary constructor." << endl;
m_p = new int(n);
}
remote_integer(const remote_integer& other) {
cout << "Copy constructor." << endl;
if (other.m_p) {
m_p = new int(*other.m_p);
} else {
m_p = NULL;
}
}
#ifdef MOVABLE
remote_integer(remote_integer&& other) {
cout << "MOVE CONSTRUCTOR." << endl;
m_p = other.m_p;
other.m_p = NULL;
}
#endif // #ifdef MOVABLE
remote_integer& operator=(const remote_integer& other) {
cout << "Copy assignment operator." << endl;
if (this != &other) {
delete m_p;
if (other.m_p) {
m_p = new int(*other.m_p);
} else {
m_p = NULL;
}
}
return *this;
}
#ifdef MOVABLE
remote_integer& operator=(remote_integer&& other) {
cout << "MOVE ASSIGNMENT OPERATOR." << endl;
if (this != &other) {
delete m_p;
m_p = other.m_p;
other.m_p = NULL;
}
return *this;
}
#endif // #ifdef MOVABLE
~remote_integer() {
cout << "Destructor." << endl;
delete m_p;
}
int get() const {
return m_p ? *m_p : 0;
}
private:
int * m_p;
};
remote_integer square(const remote_integer& r) {
const int i = r.get();
return remote_integer(i * i);
}
int main() {
remote_integer a(8);
cout << a.get() << endl;
remote_integer b(10);
cout << b.get() << endl;
b = square(a);
cout << b.get() << endl;
}
C:\Temp>cl /EHsc /nologo /W4 remote.cpp
remote.cpp
C:\Temp>remote
Unary constructor.
8
Unary constructor.
10
Unary constructor.
Copy assignment operator.
Destructor.
64
Destructor.
Destructor.
C:\Temp>cl /EHsc /nologo /W4 /DMOVABLE remote.cpp
remote.cpp
C:\Temp>remote
Unary constructor.
8
Unary constructor.
10
Unary constructor.
MOVE ASSIGNMENT OPERATOR.
Destructor.
64
Destructor.
Destructor.
這里有幾點值得注意:
·我們重載了拷貝構造函數和 move 構造函數,還重載了拷貝賦值函數和 move 賦值函數。在前面我們已經看到了當函數通過 const Type& 和 Type&& 進行重載時,會有怎樣的結果。當 move 語意可用時,b = square(a) 會自動選擇調用 move 賦值函數。
·move 構造函數和 move 賦值函數只是簡單的從 other 那里“竊取”內存,而不用動態分配內存。當“竊取”內存時,我們只是拷貝 other 的指針成員,然后再把它置為 null。于是當 other 被銷毀時,析構函數什么也不做。
·拷貝賦值函數和 move 賦值函數都需要進行自我賦值檢查,為何拷貝賦值函數需要進行自我賦值檢查是廣為人知的。這是因為像 int 這樣的內建數據(POD)類型能夠正確地自我賦值(如:x = x ),因此,用戶自定義的數據類型理應也可以正確地自我賦值。自我賦值實際上在手寫代碼里面是不存在的,但是在類似 std::sort() 之類的算法中,卻很常見。在 C++0x 中,像 std::sort() 之類的算法能夠通過挪動而非拷貝元素來實現。在這里(move 賦值函數)也需要進行自我賦值檢查。
這時,你可能會想它們( move 拷貝構造函數和 move 賦值函數)與編譯器自動生成(標準中用詞“隱式聲明”)的默認拷貝構造函數和默認賦值函數有什么相互影響呢。
·永遠不會自動生成 move 構造函數和 move 賦值函數。
·用戶聲明的構造函數,拷貝構造函數和 move 構造函數會抑制住默認構造函數的自動生成。
·用戶聲明的拷貝構造函數會抑制住默認拷貝構造函數的自動生成,但是用戶聲明的 move 構造函數做不到。
·用戶聲明的拷貝賦值函數會抑制住默認拷貝賦值函數的自動生成,但是用戶聲明的 move 賦值函數做不到。
基本上,除了聲明 move 構造函數會抑制默認構造函數的自動生成以外,自動生成規則不影響 move 語義。
( 轉載時請注明作者和出處。未經許可,請勿用于商業用途)