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