c++之make_shared

前言


shared_ptr<string> p1 = make_shared<string>(10, '9');  
 
shared_ptr<string> p2 = make_shared<string>("hello");  
 
shared_ptr<string> p3 = make_shared<string>(); 

使用

優點

儘量使用make_shared初始化
C++11 中引入了智能指針, 同時還有一個模板函數 std::make_shared 可以返回一個指定類型的 std::shared_ptr, 那與 std::shared_ptr 的構造函數相比它能給我們帶來什麼好處呢 ?

make_shared初始化的優點

提高性能

shared_ptr 需要維護引用計數的信息:
強引用, 用來記錄當前有多少個存活的 shared_ptrs 正持有該對象. 共享的對象會在最後一個強引用離開的時候銷燬( 也可能釋放).
弱引用, 用來記錄當前有多少個正在觀察該對象的 weak_ptrs. 當最後一個弱引用離開的時候, 共享的內部信息控制塊會被銷燬和釋放 (共享的對象也會被釋放, 如果還沒有釋放的話).
如果你通過使用原始的 new 表達式分配對象, 然後傳遞給 shared_ptr (也就是使用 shared_ptr 的構造函數) 的話, shared_ptr 的實現沒有辦法選擇, 而只能單獨的分配控制塊:

在這裏插入圖片描述

如果選擇使用 make_shared 的話, 情況就會變成下面這樣:
在這裏插入圖片描述
std::make_shared(比起直接使用new)的一個特性是能提升效率。使用std::make_shared允許編譯器產生更小,更快的代碼,產生的代碼使用更簡潔的數據結構。考慮下面直接使用new的代碼:

std::shared_ptr<Widget> spw(new Widget);

很明顯這段代碼需要分配內存,但是它實際上要分配兩次。每個std::shared_ptr都指向一個控制塊,控制塊包含被指向對象的引用計數以及其他東西。這個控制塊的內存是在std::shared_ptr的構造函數中分配的。因此直接使用new,需要一塊內存分配給Widget,還要一塊內存分配給控制塊。

如果使用std::make_shared來替換

auto spw = std::make_shared<Widget>();

一次分配就足夠了。這是因爲std::make_shared申請一個單獨的內存塊來同時存放Widget對象和控制塊。這個優化減少了程序的靜態大小,因爲代碼只包含一次內存分配的調用,並且這會加快代碼的執行速度,因爲內存只分配了一次。另外,使用std::make_shared消除了一些控制塊需要記錄的信息,這樣潛在地減少了程序的總內存佔用。

對std::make_shared的效率分析可以同樣地應用在std::allocate_shared上,所以std::make_shared的性能優點也可以擴展到這個函數上。

異常安全

我們在調用processWidget的時候使用computePriority(),並且用new而不是std::make_shared:

processWidget(std::shared_ptr<Widget>(new Widget),  //潛在的資源泄露 
              computePriority());

就像註釋指示的那樣,上面的代碼會導致new創造出來的Widget發生泄露。那麼到底是怎麼泄露的呢?調用代碼和被調用函數都用到了std::shared_ptr,並且std::shared_ptr就是被設計來阻止資源泄露的。當最後一個指向這兒的std::shared_ptr消失時,它們會自動銷燬它們指向的資源。如果每個人在每個地方都使用std::shared_ptr,那麼這段代碼是怎麼導致資源泄露的呢?

答案和編譯器的翻譯有關,編譯器把源代碼翻譯到目標代碼,在運行期,函數的參數必須在函數被調用前被估值,所以在調用processWidget時,下面的事情肯定發生在processWidget能開始執行之前:

表達式“new Widget”必須被估值,也就是,一個Widget必須被創建在堆上。
std::shared_ptr(負責管理由new創建的指針)的構造函數必須被執行。
computePriority必須跑完。
編譯器不需要必須產生這樣順序的代碼。但“new Widget”必須在std::shared_ptr的構造函數被調用前執行,因爲new的結構被用爲構造函數的參數,但是computePriority可能在這兩個調用前(後,或很奇怪地,中間)被執行。也就是,編譯器可能產生出這樣順序的代碼:

執行“new Widget”。
執行computePriority。
執行std::shared_ptr的構造函數。

如果這樣的代碼被產生出來,並且在運行期,computePriority產生了一個異常,則在第一步動態分配的Widget就會泄露了,因爲它永遠不會被存放到在第三步纔開始管理它的std::shared_ptr中。

使用std::make_shared可以避免這樣的問題。調用代碼將看起來像這樣:

processWidget(std::make_shared<Widget>(),       //沒有資源泄露
              computePriority());           

在運行期,不管std::make_shared或computePriority哪一個先被調用。如果std::make_shared先被調用,則在computePriority調用前,指向動態分配出來的Widget的原始指針能安全地被存放到被返回的std::shared_ptr中。如果computePriority之後產生一個異常,std::shared_ptr的析構函數將發現它持有的Widget需要被銷燬。並且如果computePriority先被調用併產生一個異常,std::make_shared就不會被調用,因此這裏就不需要考慮動態分配的Widget了。

如果使用std::unique_ptr和std::make_unique來替換std::shared_ptr和std::make_shared,事實上,會用到同樣的理由。因此,使用std::make_unique代替new就和“使用std::make_shared來寫出異常安全的代碼”一樣重要。

缺點

構造函數是保護或私有時,無法使用 make_shared
make_shared 雖好, 但也存在一些問題, 比如, 當我想要創建的對象沒有公有的構造函數時, make_shared 就無法使用了, 當然我們可以使用一些小技巧來解決這個問題, 比如這裏 How do I call ::std::make_shared on a class with only protected or private constructors?

對象的內存可能無法及時回收
make_shared 只分配一次內存, 這看起來很好. 減少了內存分配的開銷. 問題來了, weak_ptr 會保持控制塊(強引用, 以及弱引用的信息)的生命週期, 而因此連帶着保持了對象分配的內存, 只有最後一個 weak_ptr 離開作用域時, 內存纔會被釋放. 原本強引用減爲 0 時就可以釋放的內存, 現在變爲了強引用, 若引用都減爲 0 時才能釋放, 意外的延遲了內存釋放的時間. 這對於內存要求高的場景來說, 是一個需要注意的問題.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章