?
?移動語義是從C++11開始引入的一項全新功能。本文將為您撥開云霧,讓您對移動語義有個全面而深入的理解,希望本文對你理解移動語義提供一點經驗和指導。
一、為什么要有移動語義
(一)從拷貝說起
我們知道,C++中有拷貝構造函數和拷貝賦值運算符。那既然是拷貝,聽上去就是開銷很大的操作。沒錯,所謂拷貝,就是申請一塊新的內存空間,然后將數據復制到新的內存空間中。如果一個對象中都是一些基本類型的數據的話,由于數據量很小,那執行拷貝操作沒啥毛病。但如果對象中涉及其他對象或指針數據的話,那么執行拷貝操作就可能會是一個很耗時的過程。
我們來看一個例子。假設我們有個類,該類中有一個string類型的成員變量,定義如下:
?
class MyClass{public: MyClass(const std::string& s) : str{ s } {}; private: std::string str;}; MyClass A{ "hello" };
?
當我們新建一個該類的對象A,并傳遞參數“hello”時,對象A的成員變量str中會存儲字符串“hello”。而為了存儲字符串,string類型會為其分配內存空間。因此,當前內存中的數據如圖所示:
現在,當我們定義了一個該類的新對象B,且把對象A賦值給對象B時,會發生什么?即,我們執行如下語句:
?
MyClass B = A;
?
當拷貝發生時,為了讓B對象中的成員變量str也能夠存儲字符串“hello”,string類型會為其分配內存空間,并將對象A的str中存儲的數據復制過來。因此,經過拷貝操作后,此時內存中的數據如圖所示:
這個拷貝操作無可厚非,畢竟我們希望A對象和B對象是完全獨立無關的對象,對B對象的修改不會影響A對象,反之亦然。
(二)需要移動語義的情況
既然拷貝操作沒毛病,那為什么要新增移動語義呢。因為在一些情況下,我們可能確實不需要拷貝操作??紤]下面一個例子:
?
class MyClass{public: MyClass(const std::string& s) : str{ s } {}; private: std::string str;}; std::vectormyClasses;MyClass tmp{ "hello" };myClasses.push_back(tmp);myClasses.push_back(tmp);
?
在這個例子中,我們創建了一個容器以及一個MyClass對象tmp,我們將tmp對象添加到容器中2次。每次添加時,都會發生一次拷貝操作。最終內存中的數據如圖所示:
現在問題來了,tmp對象在被添加到容器中2次之后,就不需要了,也就是說,它的生命期即將結束。那么聰明的你一定想到了,既然tmp對象不再需要了,那么第2次將其添加到容器中的操作是不是就可以不執行拷貝操作了,而是讓容器直接取tmp對象的數據繼續用。沒錯,這時,就需要移動語義帥氣登場了!
(三)移動語義帥氣登場
所謂移動語義,就像其字面意思一樣,即把數據從一個對象中轉移到另一個對象中,從而避免拷貝操作所帶來的性能損耗。
那么在上面的例子中,我們如何觸發移動語義呢?很簡單,我們只需要使用std::move函數即可。有關std::move函數,就是另一個話題了,這里我們不深入探討。我們只需要知道,通過std::move函數,我們可以告知編譯器,某個對象不再需要了,可以把它的數據轉移給其他需要的對象用。
我們來改造下之前的例子:
?
class MyClass{public: MyClass(const std::string& s) : str{ s } {}; // 假設已經實現了移動語義 private: std::string str;}; std::vectormyClasses;MyClass tmp{ "hello" };myClasses.push_back(tmp);myClasses.push_back(std::move(tmp)); // 看這里
?
由于我們還沒講到移動語義的實現,因此這里先假設MyClass類已經實現了移動語義。我們改動的是最后一行代碼,由于我們不再需要tmp對象,因此通過使用std::move函數,我們讓myClasses容器直接轉移tmp對象的數據為已用,而不再需要執行拷貝操作了。
通過數據轉移,我們避免了一次拷貝操作,最終內存中的數據如圖所示:
至此,我們可以了解到,C++11引入移動語義可以在不需要拷貝操作的場合執行數據轉移,從而極大的提升程序的運行性能。
二、移動語義的實現
在了解了為什么要有移動語義之后,接著我們就該來看看它該如何實現。
(一)左值引用與右值引用
在學習如何實現移動語義之前,我們需要先了解2個概念,即“左值引用”與“右值引用”。
為了支持移動語義,C++11引入了一種新的引用類型,稱為“右值引用”,使用“&&”來聲明。而我們最常使用的,使用“&”聲明的引用,現在則稱為“左值引用”。
右值引用能夠引用沒有名稱的臨時對象以及使用std::move標記的對象:
?
int val{ 0 };int&& rRef0{ getTempValue() }; // OK,引用臨時對象int&& rRef1{ val }; // Error,不能引用左值int&& rRef2{ std::move(val) }; // OK,引用使用std::move標記的對象
?
移動語義的實現需要用到右值引用,我們在后文會詳細的說。現在我們需要知道,以下2種情況會讓編譯器將對象匹配為右值引用:
一個在語句執行完畢后就會被自動銷毀的臨時對象;
由std::move標記的非const對象。
讓編譯器將對象匹配為右值引用,是一切的基礎。
(二)區分拷貝操作與移動操作
我們回到上文的例子,對于myClasses容器的第一次push_back,我們期望執行的是拷貝操作,而對于myClasses容器的第二次push_back,由于之后我們不再需要tmp對象了,因此我們期望執行的是移動操作:
?
class MyClass{public: MyClass(const std::string& s) : str{ s } {}; // 假設已經實現了移動語義 private: std::string str;}; std::vectormyClasses;MyClass tmp{ "hello" };myClasses.push_back(tmp); // 這里執行拷貝操作,將tmp中的數據拷貝給容器中的元素myClasses.push_back(std::move(tmp)); // 這里執行移動操作,容器中的元素直接將tmp的數據轉移給自己
?
現在我們已經知道,移動操作執行的是對象數據的轉移,那么它一定是與拷貝操作不一樣的。因此,為了能夠將拷貝操作與移動操作區分執行,就需要用到我們上一節的主題:左值引用與右值引用。
因此,對于容器的push_back函數來說,它一定針對拷貝操作和移動操作有不同的重載實現,而重載用到的即是左值引用與右值引用。偽代碼如下:
?
class vector{public: void push_back(const MyClass& value) // const MyClass& 左值引用 { // 執行拷貝操作 } void push_back(MyClass&& value) // MyClass&& 右值引用 { // 執行移動操作 }};
?
通過傳遞左值引用或右值引用,我們就能夠根據需要調用不同的push_back重載函數了。那么下一個問題來了,我們知道std::vector是模板類,可以用于任意類型。所以,std::vector不可能自己去實現拷貝操作或移動操作,因為它不知道自己會用在哪些類型上。因此,std::vector真正做的,是委托具體類型自己去執行拷貝操作與移動操作。
(三)移動構造函數
當通過push_back向容器中添加一個新的元素時,如果是通過拷貝的方式,那么對應執行的會是容器元素類型的拷貝構造函數。關于拷貝構造函數,它是C++一直以來都包含的功能,相信大家已經很熟悉了,因此在這里就不展開了。
當通過push_back向容器中添加一個新的元素時,如果是通過移動的方式,那么對應執行的會是容器元素類型的“移動構造函數”(敲黑板,劃重點)。
移動構造函數是C++11引入的一種新的構造函數,它接收右值引用。以我們前文的MyClass例子來說,為其定義移動構造函數:
?
class MyClass{public: // 移動構造函數 MyClass(MyClass&& rValue) noexcept // 關于noexcept我們稍后會介紹 : str{ std::move(rValue.str) } // 看這里,調用std::string類型的移動構造函數 {} MyClass(const std::string& s) : str{ s } {} private: std::string str;};
?
在移動構造函數中,我們要做的就是轉移成員數據。我們的MyClass有一個std::string類型的成員,該類型自身實現了移動語義,因此我們可以繼續調用std::string類型的移動構造函數。
在有了移動構造函數之后,我們就可以在需要時通過它來創建新的對象,從而避免拷貝操作的開銷。以如下代碼為例:
?
MyClass tmp{ "hello" };MyClass A{ std::move(tmp) }; // 調用移動構造函數
?
首先我們創建了一個tmp對象,接著我們通過tmp對象來創建A對象,此時傳遞給構造函數的參數為std::move(tmp)。還記得我們前文提及的編譯器匹配右值引用的情況之一嘛,即由std::move標記的非const對象,因此編譯器會調用執行移動構造函數,我們就完成了將tmp對象的數據轉移到對象A上的操作:
(四)自己動手實現移動語義
在前文的MyClass例子中,我們將移動操作交由std::string類型去完成。那如果我們的類有成員數據需要我們自己去實現數據轉移的話,通常該怎么做呢?
我們來舉個例子,假設我們定義的類型中包含了一個int類型的數據以及一個char*類型的指針:
?
class MyClass{public: MyClass() : val{ 998 } { name = new char[] { "Peter" }; } ~MyClass() { if (nullptr != name) { delete[] name; name = nullptr; } } private: int val; char* name;}; MyClass A{};
?
當我們創建一個MyClass的對象A時,它在內存中的布局如圖所示:
現在我們來為MyClass類型實現移動構造函數,代碼如下所示:
?
class MyClass{public: MyClass() : val{ 998 } { name = new char[] { "Peter" }; } // 實現移動構造函數 MyClass(MyClass&& rValue) noexcept : val{ std::move(rValue.val) } // 轉移數據 { rValue.val = 0; // 清除被轉移對象的數據 name = rValue.name; // 轉移數據 rValue.name = nullptr; // 清除被轉移對象的數據 } ~MyClass() { if (nullptr != name) { delete[] name; name = nullptr; } } private: int val; char* name;}; MyClass A{};MyClass B{ std::move(A) }; // 通過移動構造函數創建新對象B
?
還記得移動語義的精髓嘛?數據拿過來用就完事兒了。因此,在移動構造函數中,我們將傳入對象A的數據轉移給新創建的對象B。同時,還需要關注的重點在于,我們需要把傳入對象A的數據清除,不然就會產生多個對象共享同一份數據的問題。被轉移數據的對象會處于“有效但未定義(valid but unspecified)”的狀態(后文會介紹)。
通過移動構造函數創建對象B之后,內存中的布局如圖所示:
(五)移動賦值運算符
與拷貝構造函數和拷貝賦值運算符一樣,除了移動構造函數之外,C++11還引入了移動賦值運算符。移動賦值運算符也是接收右值引用,它的實現和移動構造函數基本一致。在移動賦值運算符中,我們也是從傳入的對象中轉移數據,并將該對象的數據清除:
?
class MyClass{public: MyClass() : val{ 998 } { name = new char[] { "Peter" }; } MyClass(MyClass&& rValue) noexcept : val{ std::move(rValue.val) } { rValue.val = 0; name = rValue.name; rValue.name = nullptr; } // 移動賦值運算符 MyClass& operator=(MyClass&& myClass) noexcept { val = myClass.val; myClass.val = 0; name = myClass.name; myClass.name = nullptr; return *this; } ~MyClass() { if (nullptr != name) { delete[] name; name = nullptr; } } private: int val; char* name;}; MyClass A{};MyClass B{};B = std::move(A); // 使用移動賦值運算符將對象A賦值給對象B
?
三、移動構造函數和移動賦值運算符
? ? ? ? ? ? ? ? ? ?的生成規則
在C++11之前,我們擁有4個特殊成員函數,即構造函數、析構函數、拷貝構造函數以及拷貝賦值運算符。從C++11開始,我們多了2個特殊成員函數,即移動構造函數和移動賦值運算符。
本節將介紹移動構造函數和移動賦值運算符的生成規則。
(一)deleted functions
在細說移動構造函數和移動賦值運算符的生成規則之前,我們先要說一說“已刪除的函數(deleted functions)”。
在C++11中,可以使用語法=delete;來將函數定義為“已刪除”。任何使用“已刪除”函數的代碼都會產生編譯錯誤:
?
class MyClass{public: void Test() = delete;}; MyClass value;value.Test(); // 編譯錯誤:attempting to reference a deleted function
?
在之后的介紹中,我們需要關注到的點是在特定情況下,編譯器會將移動構造函數和移動賦值運算符定義為deleted。
現在讓我們進入主題,正式開始吧。
(二)默認情況下,我們擁有一切
我們知道,在C++11之前,如果我們定義一個空類,編譯器會自動為我們生成構造函數、析構函數、拷貝構造函數以及拷貝賦值運算符。該特性在移動語義上得以延伸。在C++11之后,如果我們定義一個空類,除了之前的4個特殊成員函數,編譯器還會為我們生成移動構造函數和移動賦值運算符:
?
class MyClass{}; MyClass A{}; // OK,執行編譯器默認生成的構造函數MyClass B{ A }; // OK,執行編譯器默認生成的拷貝構造函數MyClass C{ std::move(A) }; // OK,執行編譯器默認生成的移動構造函數
?
(三)當我們定義了拷貝操作之后
如果我們在類中定義了拷貝構造函數或者拷貝賦值運算符,那么編譯器就不會自動生成移動構造函數和移動賦值運算符。此時,如果調用移動語義的話,由于編譯器沒有自動生成,因此會轉而執行拷貝操作:
?
class MyClass{public: MyClass() {} // 我們定義了拷貝構造函數,這會禁止編譯器自動生成移動構造函數和移動賦值運算符 MyClass(const MyClass& value) {}}; MyClass A{};MyClass B{ std::move(A) }; // 執行的是拷貝構造函數來創建對象B
?
(四)析構函數登場
析構函數的情況和定義拷貝操作一致,如果我們在類中定義了析構函數,那么編譯器也不會自動生成移動構造函數和移動賦值運算符。此時,如果調用移動語義的話,同樣會轉而執行拷貝操作:
?
class MyClass{public: // 我們定義了析構函數,這會禁止編譯器自動生成移動構造函數和移動賦值運算符 ~MyClass() {}}; MyClass A{};MyClass B{ std::move(A) }; // 執行的是拷貝構造函數來創建對象B
?
析構函數有一點值得注意,許多情況下,當一個類需要作為基類時,都需要聲明一個virtual析構函數,此時需要特別留意是不是應該手動的為該類定義移動構造函數以及移動賦值運算符。此外,當子類派生時,如果子類沒有實現自己的析構函數,那么將不會影響移動構造函數以及移動賦值運算符的自動生成:
?
class MyBaseClass{public: virtual ~MyBaseClass() {}}; class MyClass : MyBaseClass // 子類沒有實現自己的析構函數{}; MyClass A{};MyClass B{ std::move(A) }; // 這里將執行編譯器自動生成的移動構造函數
?
(五)移動構造函數和移動賦值運算符的相互影響
如果我們在類中定義了移動構造函數,那么編譯器就不會為我們自動生成移動賦值運算符。反之,如果我們在類中定義了移動賦值運算符,那么編譯器也不會為我們自動生成移動構造函數。
之前我們提到,如果我們在類中定義了拷貝構造函數、拷貝賦值運算符或者析構函數,那么編譯器不會為我們生成移動構造函數與移動賦值運算符。此時如果執行移動語義,會轉而執行拷貝操作。但這里不同,以移動構造函數為例,如果我們定義了移動構造函數,那么編譯器不會為我們自動生成移動賦值運算符,此時,移動賦值運算符的調用并不會轉而執行拷貝賦值運算符,而是會產生編譯錯誤:
?
class MyClass{public: MyClass() {} // 我們定義了移動構造函數,這會禁止編譯器自動生成移動賦值運算符,并且對移動賦值運算符的調用會產生編譯錯誤 MyClass(MyClass&& rValue) noexcept {}}; MyClass A{};MyClass B{};B = std::move(A); // 對移動賦值運算符的調用產生編譯錯誤:attempting to reference a deleted function
?
通過編譯器的報錯信息我們可以推斷,如果我們定義了移動構造函數,那么移動賦值運算符會被編譯器定義為“已刪除的函數”,反之,如果我們定義了移動賦值運算符,那么移動構造函數也會被編譯器定義為“已刪除的函數”。
(六)小結
通過以上的介紹說明,我們對移動構造函數以及移動賦值運算符的自動生成以及可用性有了理解和掌握。我們現在將其整理為表格,從而能夠更加清晰而全面的一覽無遺:
四、noexcept
在前文我們實現移動構造函數以及移動賦值運算符時,我們使用了noexcept說明符。本節我們就來聊聊何為noexcept。
(一)為什么需要noexcept
為了說明為什么需要noexcept,我們還是從一個例子出發。我們定義MyClass類,并且我們先不對MyClass類的移動構造函數使用noexcept:
?
class MyClass{public: MyClass() {} MyClass(const MyClass& lValue) { std::cout << "拷貝構造函數" << std::endl; } MyClass(MyClass&& rValue) // 注意這里,我們沒有對移動構造函數使用noexcept { std::cout << "移動構造函數" << std::endl; } private: std::string str{ "hello" };};
?
接著,我們創建一個MyClass的對象A,并且將其往classes容器中添加2次:
?
MyClass A{};std::vectorclasses;classes.push_back(A);classes.push_back(A);
?
現在,我們來梳理一下流程。classes容器在定義時默認會申請1個元素的內存空間。當第1次執行classes.push_back(A);時,對象A會被拷貝到容器第1個元素的位置:
當第2次執行classes.push_back(A);時,由于classes容器已沒有多余的內存空間,因此它需要分配一塊新的內存空間。在分配新的內存空間之后,classes容器會做2個操作:將對象A拷貝到容器第2個元素的位置,以及將之前的元素放到新的內存空間中容器第1個元素的位置:
細心的小伙伴一定發現了,如上圖所示那般,老的元素是被拷貝到新的內存空間中的。是的,classes容器確實使用的是拷貝構造函數。那么此時我們會想到,既然classes容器已經不需要之前的內存中的數據了,那么將老數據放到新的內存空間中應該使用移動語義,而非拷貝操作。
那么為什么classes容器沒有使用移動語義呢?
此時,我們需要提及一個概念,即“強異常保證(strong exception guarantee)”。所謂強異常保證,即當我們調用一個函數時,如果發生了異常,那么應用程序的狀態能夠回滾到函數調用之前:
那么強異常保證和決定使用移動語義或拷貝操作又有什么關系呢?
這是因為容器的push_back函數是具備強異常保證的,也就是說,當push_back函數在執行操作的過程中(由于內存不足需要申請新的內存、將老的元素放到新內存中等),如果發生了異常(內存空間不足無法申請等),push_back函數需要確保應用程序的狀態能夠回滾到調用它之前。以上面的例子來說,當第2次執行classes.push_back(A);時,如果發生了異常,應用程序的狀態會回滾到第1次執行classes.push_back(A);之后,即classes容器中只有一個元素。
由于我們的移動構造函數沒有使用noexcept說明符,也就是我們沒有保證移動構造函數不會拋出異常。因此,為了確保強異常保證,就只能使用拷貝構造函數了。那么拷貝構造函數同樣沒有保證不會拋出異常,為什么就能用呢?這是因為拷貝構造函數執行之后,被拷貝對象的原始數據是不會丟失的。因此,即使發生異常需要回滾,那些已經被拷貝的對象仍然完整且有效。但移動語義就不同了,被移動對象的原始數據是會被清除的,因此如果發生異常,那些已經被移動的對象的數據就沒有了,找不回來了,也就無法完成狀態回滾了。
(二)為移動語義使用noexcept說明符
在了解了以上的規則后,我們就清楚了,要想使用移動構造函數來將老的元素放到新的內存中,我們就需要告知編譯器,我們的移動構造函數不會拋出異常,可以放心使用,這就是通過noexcept說明符完成的。
我們來修改下MyClass類的移動構造函數,為其加上noexcept說明符:
?
class MyClass{public: MyClass() {} MyClass(const MyClass& lValue) { std::cout << "拷貝構造函數" << std::endl; } MyClass(MyClass&& rValue) noexcept // 注意這里,為移動構造函數使用noexcept { std::cout << "移動構造函數" << std::endl; } private: std::string str{ "hello" };};
?
現在,我們再次執行上文的例子,會發現使用的是移動構造函數來創建新的內存中的元素了:
關于noexcept說明符,是個龐大的話題,這里我們只是粗略的提及和移動語義有關的部分。值得注意的是,noexcept說明符是我們對于不會拋出異常的保證,如果在執行的過程中有異常被拋出了,應用程序將會直接終止執行。
五、使用移動語義時需要注意的其他內容
在最后一節,我們聊聊與移動語義相關的一些額外內容。
(一)編譯器生成的移動構造函數和移動賦值運算符
前文我們提及,在特定情況下,編譯器會為我們自動生成移動構造函數和移動賦值運算符。在自動生成的函數中,編譯器執行的是逐成員的移動語義。
假設我們的類包含一個int類型和一個std::string類型的成員:
?
class MyClass{private: int val; std::string str;};
?
那么編譯器為我們自動生成的移動構造函數和移動賦值運算符類似于如下所示:
?
class MyClass{public: // 編譯器自動生成的移動構造函數類似這樣,執行逐成員的移動語義 MyClass(MyClass&& rValue) noexcept : val{ std::move(rValue.val) } , str{ std::move(rValue.str) } {} // 編譯器自動生成的移動賦值運算符類似這樣,執行逐成員的移動語義 MyClass& operator=(MyClass&& rValue) noexcept { val = std::move(rValue.val); str = std::move(rValue.str); return *this; } private: int val; std::string str;};
?
了解編譯器自動生成的移動構造函數以及移動賦值運算符之后,我們就會對何時應該自己去實現有個很清晰的認識。
(二)被移動對象的狀態
當一個對象被移動之后,該對象仍然是有效的,你可以繼續使用它,最終它會被銷毀,執行析構函數。
C++在其文檔中表明,所有標準庫中的對象,當被移動之后,會處于一個“有效但未定義的狀態(valid but unspecified state)”。
C++并沒有強制的規定限制被移動對象必須處于什么狀態,并且當類型需要滿足不同用途時它的要求也不一致(例如用于key的類型要求被移動對象仍然能夠進行排序),因此我們在實現自己的類型時需要根據具體情況來分析。但通常來說,我們應該盡可能的貼近C++標準庫中的類型規范。但不管如何,以下這一點是我們必須考慮的:
保證被移動對象能夠被正確的析構。
為什么必須保證這一點呢?這是因為被移動對象只是處于一個特殊的狀態,對于運行時來說,仍然是有效的,最終也會執行析構函數進行銷毀。
例如在之前我們的MyClass類型定義中,當我們執行移動語義后,被移動對象的name指針會被置空。當執行析構函數時,我們就可以簡單的通過判空來避免指針的無效釋放,這就確保了被移動對象能夠正確的析構:
?
?
?
class MyClass{public: MyClass() : val{ 998 } { name = new char[] { "Peter" }; } MyClass(MyClass&& rValue) noexcept : val{ std::move(rValue.val) } { rValue.val = 0; name = rValue.name; rValue.name = nullptr; // 置空被移動對象的指針 } MyClass& operator=(MyClass&& myClass) noexcept { val = myClass.val; myClass.val = 0; name = myClass.name; myClass.name = nullptr; // 置空被移動對象的指針 return *this; } ~MyClass() { if (nullptr != name) // 通過判空來避免指針的無效釋放 { delete[] name; name = nullptr; } } private: int val; char* name;};
?
(三)避免非必要的std::move調用
在C++中,存在稱為“NRVO(named return value optimization,命名返回值優化)”的技術,即如果函數返回一個臨時對象,則該對象會直接給函數調用方使用,而不會再創建一個新對象。聽起來有點晦澀,我們來看一個例子:
?
class MyClass{}; MyClass GetTemporary(){ MyClass A{}; return A;} MyClass myClass = GetTemporary(); // 注意這里
?
在上面的例子中,GetTemporary函數會創建一個臨時的MyClass對象A,接著在函數結束時返回。在沒有NRVO的情況下,當執行語句MyClass myClass=GetTemporary();時,會調用MyClass類的拷貝構造函數,通過對象A來拷貝創建myClass對象。因此,整個流程如圖所示:
我們可以發現,在創建完myClass對象之后,對象A就被銷毀了,這無疑是一種浪費。因此,編譯器會啟用NRVO,直接讓myClass對象使用對象A。這樣一來,在整個過程中,我們只有一次創建對象A時構造函數的調用開銷,省去了拷貝構造函數以及析構函數的調用開銷:
為NRVO點贊!
此時,可能有細心的小伙伴已經發現了,這種返回臨時對象的情況不就是移動語義發揮的場景嘛。沒錯,機智的你是不是會想到如下的修改:
?
MyClass GetTemporary(){ MyClass A{}; return std::move(A); // 使用移動語義}
?
這樣一來,通過移動語義,即使沒有NRVO,也可以避免拷貝操作。乍看上去沒啥毛病,但我們忽略了一種情況,那就是返回的對象類型并沒有實現移動語義。
讓我們來分析一下這種情況,我們改寫一下MyClass類:
?
class MyClass{public: ~MyClass() // 注意這里,通過聲明析構函數,我們禁止了編譯器去實現默認移動構造函數 {}};
?
現在,MyClass類型沒有實現移動語義,當我們執行語句MyClass myClass=GetTemporary();時,編譯器沒有辦法調用移動構造函數來創建myClass對象。同時,遺憾的是,由于std::move(A)返回的類型是MyClass&&,與函數的返回類型MyClass不一致,因此編譯器也不會使用NRVO。最終,編譯器只能調用拷貝構造函數來創建myClass對象。
因此,當返回局部對象時,我們不用畫蛇添足,直接返回對象即可,編譯器會優先使用最佳的NRVO,在沒有NRVO的情況下,會嘗試執行移動構造函數,最后才是開銷最大的拷貝構造函數。
六、總結
本文向您闡述了C++中的移動語義,從緣由、定義到實現,以及其他的一些相關細節內容。相信您在看完本文后對C++的移動語義會有更加全面而深刻的認識,可以向媽媽匯報了!
評論
查看更多