移動語義
移動語義(Move Semantics)應(yīng)該是C++11標準帶來的最大改進。通過移動語義,我們可以實現(xiàn)更為細致的內(nèi)存管理。比如,從一個之后不再使用的對象復(fù)制數(shù)據(jù)時,我們可以通過移動語義手動回收這個對象可以被我們直接利用的內(nèi)存數(shù)據(jù),避免大規(guī)模的內(nèi)存復(fù)制操作。對于移動操作的時間復(fù)雜度是常數(shù)時間的情況(vector,map,unordered_map,string等標準庫對象都是這樣),我們的性能收益是巨大的。那么,問題是我們?nèi)绾沃酪粋€對象在之后不再使用?這就需要了解右值(rvalue)的概念,它表示一個臨時的,在之后不能被訪問的值。對于下面的代碼,user是一個左值(lvalue),字符串字面量"Skywalker"是一個右值(rvalue)。我們可以在這行代碼運行之后訪問user變量,但卻不能訪問到原始的字符串字面量"Skywalker"(讀者思考下為什么?)。
[圖片上傳失敗...(image-b9b24c-1614136368875)]

下圖通過一個生活中的場景來說明移動語義(Move Semantics)。圖1中,藍色小人通過復(fù)制構(gòu)造復(fù)制(重新生成或初始化,生成和初始化需要耗費大量時間)了紅色小人房子里的家具,圖2中,藍色小人通過移動構(gòu)造繼承了紅色小人房子里的家具。顯然,對于藍色小人來說,繼承紅色小人的家具比自己購買新的一模一樣的家具的代價要小(不用花錢_),換成代碼來說,就是繼承比復(fù)制更高效。
[圖片上傳失敗...(image-949699-1614136326416)]
[圖片上傳失敗...(image-f2030e-1614136326416)]
下面給出了復(fù)制構(gòu)造和移動構(gòu)造的一個示例代碼,當(dāng)要復(fù)制的對象是一個右值(rvalue),會調(diào)用移動構(gòu)造函數(shù),其它情況調(diào)用復(fù)制構(gòu)造函數(shù)??梢钥闯?,一個典型的復(fù)制構(gòu)造函數(shù)和典型的移動構(gòu)造函數(shù)之間的不同:移動構(gòu)造函數(shù)的參數(shù)沒有使用const關(guān)鍵字,參數(shù)的變量名前有兩個&符號,用來表示右值引用。因為rhs是右值引用,我們可以認為在之后它不會再被使用,所以移動構(gòu)造函數(shù)直接復(fù)制了對象數(shù)據(jù)的內(nèi)存指針,沒有進行內(nèi)存分配和數(shù)據(jù)的深copy。除了移動構(gòu)造函數(shù),我們還可以在賦值運算上這樣使用移動語義。
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="c++" cid="n9" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; background: rgb(51, 51, 51); position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">// Copy constructor
string(const string& rhs) {
allocateSpace(myDataPtr);
deepcopy(rhs.myDataPtr, myDataPtr);
}
// Move constructor
string(string&& rhs) {
myDataPtr = rhs.myDataPtr;
rhs.myDataPtr = nullptr;
}</pre>
對于一個可引用的變量(lvalue),如果我們確定之后不會再使用它,可以使用std::move手動將其變?yōu)橛抑祬?shù)。在之后的章節(jié),我們還會討論只能進行移動操作不能進行復(fù)制操作的對象。對于被移動的對象,如果沒有重新初始化,我們不應(yīng)該使用它。下面的代碼演示了這一個過程,標準庫中的unique_ptr類生成的對象就是一個只能移動,不能復(fù)制的對象。
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="c++" cid="n12" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; background: rgb(51, 51, 51); position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">MoveOnlyObject a;
...
MoveOnlyObject b(a); // ERROR – copy constructor doesn't exist
MoveOnlyObject c(std::move(a)); // OK – ownership transferred to c. a is DEAD now
cout << *a; // RUNTIME ERROR – illegal access</pre>
上面的代碼在a對象已經(jīng)被移動后仍然訪問了它,這樣做的后果是不可預(yù)料的。我們可以向下面的代碼這樣,通過作用域來避免這種情況:
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="c++" cid="n14" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; background: rgb(51, 51, 51); position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">MoveOnlyObject c;
{
MoveOnlyObject a;
...
c = MoveOnlyObject(std::move(a));
} // can't even attempt to dereference a anymore</pre>
另一個需要牢記的地方,在向容器插入對象時,如果臨時變量以后不再使用,應(yīng)該通過std::move將其轉(zhuǎn)為右值參數(shù),避免不必要的內(nèi)存數(shù)據(jù)復(fù)制:
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="c++" cid="n16" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; background: rgb(51, 51, 51); position: relative !important; padding: 10px 10px 10px 30px; width: inherit; color: rgb(184, 191, 198); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">vector<string> importantUsers;
...
string localUser;
... // compute local user
allUsers.push_back(std::move(localUser));</pre>
關(guān)于移動語義,還有許多細節(jié)我們沒有提到:比如,&&符號在其它情形下的使用等等。另外,需要l注意的是位于棧上的內(nèi)存不能被移動操作復(fù)用,也就是說如果一個類只包含編譯器自動維護作用域的變量(類中的變量都實際在棧中占用了連續(xù)的內(nèi)存塊,而不是通過類似指針的方式引用,讀者認真思考為什么?可以考慮string對象因為會使用指針引用動態(tài)申請的內(nèi)存,所以它不是,可以從移動語義收益,而只包含char str[50]的類,不能從移動語義收益),移動操作對其不會有任何提升。盡管不能收益于移動語義,我們?nèi)钥赡転檫@樣的類添加移動構(gòu)造函數(shù),來使操作它們的代碼和其它類的代碼看上去是一致的,但實際上,編譯器會為這樣的類自動生成移動構(gòu)造,我們自己添加實際上是多此一舉。對于包含這樣的類的對象的標準庫容器,比如vector等,由于容器的數(shù)據(jù)存儲空間是動態(tài)申請的,并非來自棧上,我們?nèi)阅軓囊苿诱Z義中收益(移動操作復(fù)制的是存儲空間的內(nèi)存地址,而不是實際的每個類對象的實際數(shù)據(jù))?;诖耍瑢τ谑褂脴藴蕩烊萜?比如vector)的程序來說,可以認為升級到C++11會自動獲得一定的性能提升。
note
認真思考移動語義是如何影響代碼的性能表現(xiàn)。
使用std::move()傳遞數(shù)據(jù)的所有權(quán)給容器。
推薦將需要移動的對象定義在一個作用域中,并且在作用域的最后一行代碼移動對象,從而避免再次使用已經(jīng)被移動的對象。
右值特點:
不可修改
沒名字