C++11新標準中最重要的特性之一就是引入了支持對象移動的能力,移動語義的加持使得移動一個如容器之類的大對象的成本可以像復(fù)制一個指針一樣低廉了,于是出現(xiàn)了各種各樣的傳言:如編譯器會使用移動操作來替代拷貝操作以獲得效率上的提升,甚至說將符合C++98標準的以前的老代碼用符合C++11新標準的編譯器重新
接下來我將持續(xù)更新“深度解讀《深度探索C++對象模型》”系列,敬請期待,歡迎關(guān)注!也可以關(guān)注公眾號:iShare愛分享,自動獲得推文和全部的文章列表。
C++11新標準中最重要的特性之一就是引入了支持對象移動的能力,為了支持移動的操作,新標準引入了一種新的引用類型——右值引用,右值引用一個重要的性質(zhì)就是只能綁定到一個將要銷毀的對象。對對象執(zhí)行移動操作后要確保源對象處于可析構(gòu)的狀態(tài),源對象隨時可能被銷毀,所以程序在之后不要再去使用源對象的值,同時也要保證源對象析構(gòu)之后不會對移入對象產(chǎn)生副作用。移動語義的加持使得移動一個如容器之類的大對象的成本可以像復(fù)制一個指針一樣低廉了,于是出現(xiàn)了各種各樣的傳言:如編譯器會使用移動操作來替代拷貝操作以獲得效率上的提升,甚至說將符合C++98標準的以前的老代碼用符合C++11新標準的編譯器重新編譯一次,一行代碼未改即可獲得運行速度上質(zhì)的提升。對于種種傳聞,事實上是否如此?接下來讓我們撥開層層迷霧,來一探究竟,看完這篇文章,你的心中就會有答案。
為了支持對象的移動,新標準新增了移動構(gòu)造函數(shù)和移動賦值運算符,移動構(gòu)造函數(shù)和移動賦值運算符的情形類似,所以放在一起討論。對于傳聞中如果程序中沒有定義移動構(gòu)造函數(shù),那么編譯器就會幫助程序生成一個移動構(gòu)造函數(shù)這一說法是否可靠?我們以實際的代碼來分析一下,由于移動構(gòu)造函數(shù)需要一個右值引用作為第一個參數(shù),測試代碼中可以使用標準庫里的move函數(shù)來產(chǎn)生一個右值引用,move函數(shù)其實就是一個類型轉(zhuǎn)換,它可以把一個左值轉(zhuǎn)換成右值引用?纯聪旅娴拇a是否編譯器會合成出來移動構(gòu)造函數(shù):
#include
class Object {
int a;
};
int main() {
Object d;
Object d1 = std::move(d);
return 0;
}
把它編譯成匯編代碼看一下:
main: # @main
push rbp
mov rbp, rsp
mov dword ptr [rbp - 4], 0
mov eax, dword ptr [rbp - 8]
mov dword ptr [rbp - 16], eax
xor eax, eax
pop rbp
ret
實際上編譯器并沒有生成一個移動構(gòu)造函數(shù),甚至任何構(gòu)造函數(shù)都沒有生成。因為沒有必要,在這種情況下,編譯器可以做一些優(yōu)化,執(zhí)行按對象的成員逐個復(fù)制過去就可以了,不需要生成一個函數(shù)來做這個事情。上面匯編代碼的第5、第6行就是將對象d(存放在?臻g[rbp - 8]中)的內(nèi)容先拷貝到eax寄存器,然后再從寄存器eax拷貝到對象d1(存放在棧空間[rbp - 16]中)。
那么在什么情況下才會合成出來移動構(gòu)造函數(shù)呢?
編譯器只有在以下的這些情況下才會合成出來移動構(gòu)造函數(shù):
在上面C++代碼的Object類中增加一個std::string類型的成員,std::string是標準庫中提供的操作字符串的類,類中有定義了移動構(gòu)造函數(shù)。Object類定義如下:
class Object {
std::string s;
int a;
};
把它編譯成匯編代碼,可以看到這下匯編代碼變得很多,不光生成了Object類的移動構(gòu)造函數(shù),還有默認構(gòu)造函數(shù)和析構(gòu)函數(shù)。main函數(shù)的匯編代碼如下:
main: # @main
push rbp
mov rbp, rsp
sub rsp, 96
mov dword ptr [rbp - 4], 0
lea rdi, [rbp - 48]
call Object::Object() [base object constructor]
lea rdi, [rbp - 88]
lea rsi, [rbp - 48]
call Object::Object(Object&&) [base object constructor]
mov dword ptr [rbp - 4], 0
lea rdi, [rbp - 88]
call Object::~Object() [base object destructor]
lea rdi, [rbp - 48]
call Object::~Object() [base object destructor]
mov eax, dword ptr [rbp - 4]
add rsp, 96
pop rbp
ret
上面匯編代碼的第7行調(diào)用了Object類的默認構(gòu)造函數(shù),因為string類里也定義了默認構(gòu)造函數(shù),所以這里需要去調(diào)用它,具體分析可見另外一篇的分析文章。第10行實際上就是調(diào)用Object類的移動構(gòu)造函數(shù)了,在Object類的移動構(gòu)造函數(shù)里會去調(diào)用string類的移動構(gòu)造函數(shù)。所以可以推測出來,只有需要調(diào)用類類型成員的移動構(gòu)造函數(shù)的時候編譯器才會合成一個移動構(gòu)造函數(shù)出來,在合成的移動構(gòu)造函數(shù)中去調(diào)用它,上面的第3種情況也類似,第4和第5種情形是因為編譯器需要重設(shè)虛表指針,所以也會生成一個移動構(gòu)造函數(shù)來完成,這些情形跟合成拷貝構(gòu)造函數(shù)的機制是類似的,具體的分析可以見《編譯器背后的行為之拷貝構(gòu)造函數(shù)》這篇文章,這里就不再一一贅述了。
雖然說合成移動構(gòu)造函數(shù)的時機和合成拷貝構(gòu)造函數(shù)的類似,但是合成移動構(gòu)造函數(shù)的條件要比合成拷貝構(gòu)造函數(shù)要苛刻得多,在以下的情形中,移動構(gòu)造函數(shù)的合成將受到抑制,編譯器不會合成一個移動構(gòu)造函數(shù)出來。
有這么一個指導(dǎo)原則,叫做Rule of Three,大意是:主要你定義了拷貝構(gòu)造函數(shù)、拷貝賦值運算符、析構(gòu)函數(shù)中的一個,你就必須要全部定義它們。原因就是既然你需要自己實現(xiàn)拷貝的操作,說明這里需要管理資源,比如內(nèi)存的申請和釋放,在拷貝構(gòu)造函數(shù)里需要管理資源,意味著在拷貝賦值運算符函數(shù)里也需要,反之亦然,同時也需要在析構(gòu)函數(shù)中釋放資源。由此可以得出的推論就是如果你定義了這其中的一個函數(shù),說明有資源需要特別處理,那么編譯器合成出來的移動構(gòu)造函數(shù)可能就不是你想要的效果,甚至破壞程序的邏輯,引起潛在的bug,所以編譯器就不會合成出來移動構(gòu)造函數(shù)。
按照上面的推論,如果定義了析構(gòu)函數(shù),那么編譯器就不應(yīng)該生成拷貝構(gòu)造函數(shù)和拷貝賦值運算符了,但是C++98標準中卻留下了一個“bug“:在定義了析構(gòu)函數(shù)之后,編譯器還是會在有需要的時候合成出拷貝構(gòu)造函數(shù)和拷貝賦值運算符,C++11標準為了兼容C++98,同樣地也允許合成出來,但是對于移動構(gòu)造函數(shù)和移動賦值運算符, C++11標準中明確規(guī)定了:只要定義了析構(gòu)函數(shù),編譯器便不再合成出移動構(gòu)造函數(shù)和移動賦值運算符。
如果你的代碼中沒有定義上面的三種函數(shù),你的類中的成員也是可以移動的,編譯器在這時也為程序合成出了移動構(gòu)造函數(shù)或者移動賦值運算符,如果這一切正符合你的本意,那么這種情況下建議你,最好在你的代碼中把移動構(gòu)造函數(shù)或移動賦值運算符用=default顯示地聲明出來。原因在于,假如有一個類,類中有一個容器,容器存放了大量的數(shù)據(jù),類中沒有定義拷貝構(gòu)造函數(shù)和析構(gòu)函數(shù)等,編譯器也合成了移動構(gòu)造函數(shù),使得對象的移動非常高效。但是突然有天來個需求,需要在對象的構(gòu)造和析構(gòu)時記錄下來,于是你增加了構(gòu)造函數(shù)和析構(gòu)函數(shù)以滿足需求,但是加入代碼重新編譯之后發(fā)現(xiàn)程序執(zhí)行的效率變差了,甚至有可能差了幾個數(shù)量級,
根源在于你定義了析構(gòu)函數(shù)之后,編譯器便不再合成移動構(gòu)造函數(shù)了,而是用拷貝操作替換了移動的操作
,所以顯示地聲明它們是一種好的習(xí)慣,盡管我們不需要實現(xiàn)這個函數(shù)的代碼,所以使用
=default
讓編譯器來自動生成。
如下面的例子:
#include
#include
class Base {
public:
Base() = default;
Base(Base&& rhs) = delete;
int b;
};
class Object {
public:
Base b;
std::string s;
int a;
};
int main() {
Object d;
Object d1 = std::move(d); // 這行編譯不通過。
return 0;
}
上面的例子中,編譯器不再會生成移動構(gòu)造函數(shù)和拷貝構(gòu)造函數(shù),所以第20行的代碼將編譯不通過,因為沒有拷貝構(gòu)造函數(shù)或移動構(gòu)造函數(shù)供調(diào)用。
在某些情況下,移動構(gòu)造函數(shù)或移動賦值運算符被正確地合成出來或者由程序員定義出來了,但是程序卻并未如預(yù)期的提升運行效率,如以下的場景:
假如類中有了移動構(gòu)造函數(shù)(合成的或者用戶定義的),同時類中有一個類類型的成員,這個成員剛好存放著大量數(shù)據(jù),而此成員的類定義中沒有定義移動構(gòu)造函數(shù),因此它只可以拷貝而不能移動。當對對象實施move操作時,實際上將會對對象的每個成員依次遞歸地實施move調(diào)用,它將匹配適合這個成員的操作,即如果成員是可移動則執(zhí)行移動操作,如果不可移動的則執(zhí)行拷貝操作。所以實際上將會調(diào)用此成員的拷貝構(gòu)造函數(shù)。
另一種情形,如std::array容器,它是C++11標準新提供的容器類型,功能相當于內(nèi)建的數(shù)組,它不同于別的容器類型將數(shù)據(jù)存儲在堆中,然后使用指針指向數(shù)據(jù),移動容器只需賦值指針,然后將源指針置空即可。array容器的數(shù)據(jù)是存放在對象上,即使數(shù)組里存放的元素類型能提供移動操作,那也得需要一個個地將每個元素執(zhí)行一遍移動操作,這個時間是一個線性時間復(fù)雜度。
std::string類往往采用了小型字符串優(yōu)化(small string optimization, SSO)的實現(xiàn)手法,SSO是將小型字符串(比如長度小于15個字符)直接存儲在string對象內(nèi)的緩沖區(qū)中,超過這個長度的則存放在堆上。之所以采用SSO優(yōu)化手法,就是因為在實際應(yīng)用場景中大多數(shù)使用的字符串長度都比較短,這樣可避免頻繁地申請和釋放內(nèi)存帶來的開銷。在使用了SSO的情況下,移動一個string對象并不比較拷貝來得更快,實際上這種情況移動操作執(zhí)行的是拷貝動作。
即使類中提供的移動操作比拷貝操作的效率明顯要高得多,但是也有可能未能調(diào)用到移動操作,依然使用的是拷貝操作,導(dǎo)致實際效果效率不高的問題。比如標準庫中的vector容器,它提供了一個push_back的接口,調(diào)用此接口向容器中加入一個元素,這時有可能容器的容量滿了,需要申請一塊更大的內(nèi)存,然后把原先內(nèi)存位置的元素搬過去再銷毀掉。vector容器的實現(xiàn)者需要保證這個過程的前后狀態(tài)要保持不變,在移動元素時,如果元素的類型提供了移動功能,那么vector容器就會使用它,但是要求這個移動操作必須是noexcept的,假如移動操作不能保證是noexcept的,vector容器就不會使用它。
試想一下,假如在移動到一半的時候,這時拋出了異常,移動操作隨即停止,這時一半的元素在新空間中,一半的元素在舊的空間中,vector無法恢復(fù)到原先的狀態(tài)。拷貝操作則不會存在這個問題,假如在拷貝過程中出現(xiàn)問題,那么只需要將新空間的元素和新申請的內(nèi)存釋放掉,vector的狀態(tài)還是保持不變。
所以如果你的類型中的移動構(gòu)造函數(shù)未加上noexcept聲明,即使類型中的移動操作比對應(yīng)的拷貝操作的效率要高效得多,編譯器仍會強制去調(diào)用拷貝操作而非移動操作。因此建議當你定義自己版本的移動構(gòu)造函數(shù)或移動賦值運算符的時候,要確保不會拋出異常,并在聲明中明確加上noexcept聲明。
如果您感興趣這方面的內(nèi)容,請在微信上搜索公眾號iShare愛分享或者微信號iTechShare并關(guān)注,以便在內(nèi)容更新時直接向您推送。
機器學(xué)習(xí):神經(jīng)網(wǎng)絡(luò)構(gòu)建(下)
閱讀華為Mate品牌盛典:HarmonyOS NEXT加持下游戲性能得到充分釋放
閱讀實現(xiàn)對象集合與DataTable的相互轉(zhuǎn)換
閱讀算法與數(shù)據(jù)結(jié)構(gòu) 1 - 模擬
閱讀5. Spring Cloud OpenFeign 聲明式 WebService 客戶端的超詳細使用
閱讀Java代理模式:靜態(tài)代理和動態(tài)代理的對比分析
閱讀Win11筆記本“自動管理應(yīng)用的顏色”顯示規(guī)則
閱讀本站所有軟件,都由網(wǎng)友上傳,如有侵犯你的版權(quán),請發(fā)郵件[email protected]
湘ICP備2022002427號-10 湘公網(wǎng)安備:43070202000427號© 2013~2025 haote.com 好特網(wǎng)