##Item 21 優先使用`std::make_unique`和`std::make_shared`而不是直接使用new
我們先給`std::make_unique`以及`std::make_shared`提供一個公平的競爭環境,以此開始。`std::make_shared`是C++ 11標準的一部分,但是,遺憾的是,`std::make_unique`不是的。它剛成為C++ 14的一部分。如果你在使用C++11.不要怕,因為你可以很容易自己寫一個基本版的`std::make_unique`,我們瞧:
```cpp
template<typename T, typename... Ts>
std::unique_ptr<T> make_unique<Ts&&... params>
{
return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}
```
如你所見,`make_unique`只是完美轉發了它的參數到它要創建的對象的構造函數中去,由new出來的原生指針構造一個`std::unique_ptr`,并且將之返回。這中格式的構造不支持數組以及自定義deleter(請看 Item18),但是它說明只需稍加努力,便可自己創造出所需要的`make_unique`(備注:為了盡可能花最小大家創造出一個功能齊全的`make_unique`,搜索產生它的標準文檔,并且拷貝一份文檔中的實現。這里所需要的文檔是日期為2013-04-18,Stephan T.Lavavej所寫的N3656).請記住不要把你自己實現的版本放在命名空間std下面,因為假如說日后你升級到C++ 14的標準庫市縣,你可不想自己實現的版本和標準庫提供的版本產生沖突。
`std::make_unique`以及`std::make_shared`是3個make函數的其中2個:make函數接受任意數量的參數,然后將他們完美轉發給動態創建的對象的構造函數,并且返回指向那個對象的智能指針。第三個make函數是`std::allocate_shared`,除了第一個參數是一個用來動態分配內存的allocator對象,它表現起來就像`std::make_shared`.
即使是最普通的是否使用make函數來創建智能指針之間的比較,也表明了為什么使用make函數是比較可行的做法。考慮一下代碼:
```cpp
auto upw1(std::make_unique<Widget>());//使用make函數
std::unique_ptr<Widget> upw2(new Widget);//不使用make函數
auto spw1(std::make_shared<Widget>());//使用make函數
std::shared_ptr<Widget> spw2(new Widget);//不使用make函數
```
我已經高亮顯示了必要的差別(不好意思,它這里高亮的是Widget,在代碼里高亮暫時俺還做不到--譯者注):使用new需要重復寫一遍type,而使用make函數不需要。重復敲type違背了軟件工程中的一項基本原則:代碼重復應當避免。源代碼里面的重復會使得編譯次數增加,導致對象的代碼變得臃腫,由此產生出的code base(code base的含義請至http://en.wikipedia.org/wiki/Codebase--譯者注)變得難以改動以及維護。它經常會導致產生不一致的代碼。一個code base中的不一致代碼會導致bug.并且,敲某段代碼兩遍會比敲一遍更費事,誰不想寫程序時敲比較少的代碼呢。
第二個偏向make函數的原因是為了保證產生異常后程序的安全。設想我們有一個函數根據某個優先級來處理Widget:
```cpp
void processWidget(std::shared_ptr<Widget> spw,int priority);
```
按值傳遞`std::shared_ptr`可能看起來很可疑,但是Item41解釋了如果processWidget總是要創建一個`std::shared_ptr`的拷貝(例如,存儲在一個數據結構中,來跟蹤已經被處理過的Widget),這也是一個合理的設計.
現在我們假設有一個函數來計算相關的優先級
```cpp
int computePriority()
```
如果我們調用processWidget時,使用new而不是`std::make_shared`:
```cpp
processWidget(std::shared_ptr<Widget>(new Widget),computePriority())
//可能會導致內存泄露!
```
就像注釋里面所說的,這樣的代碼會產生因new引發的Widget對象的內存泄露。但是怎么會這樣?函數的聲明和調用函數的代碼都使用了`std::shared_ptr`,設計`std::shared_ptr`的目的就是防止內存泄露。當指向資源的最后一個`std::shared_ptr`即將離去時,資源會自動得到析構。不管是什么地方,每個人都在用`std::shared_ptr`,為什么還會發生內存泄露?
這個問題的答案和編譯器將源代碼翻譯為object code(目標代碼,想要知道object code是什么,請看這個問題http://stackoverflow.com/questions/466790/assembly-code-vs-machine-code-vs-object-code)有關系。在運行時(runtime:In computer science, run time, runtime or execution time is the time during which a program is running (executing), in contrast to other phases of a program's lifecycle such as compile time, link time and load time.)。在函數被調用前,函數的參數必須被推算出來,所以在調用processWidget的過程中,processWidget開始執行之前,下面的事情必須要發生:
* "new Widget"表達式必須被執行,即,一個Widget必須在堆上被創建
* 負責管理new所創建的指針的`std::shared_ptr<Widget>`的構造函數必須被執行
* computePriority必須被執行
并沒有要求編譯器產生出對這些操作做到按順序執行的代碼。"new Widget"必須要在std::shared_ptr的構造函數被調用之前執行,因為new的結果作為該構造函數的一個參數,因為computePriority可能在這些調用之前執行,或者之后,更關鍵的是,或者在它們之間。這樣的話,編譯器可能按如下操作的順序產生出代碼:
1. 執行"new Widget".
2. 執行computePriority.
3. 執行std::shared_ptr的構造函數.
如果這樣的代碼在runtime被產生出來,computePriority產生出了一個異常,那么在Step 1中動態分配的Widget可能會產生泄漏.因為它永遠不會存儲在Step 3中產生的本應負責管理它的`std::shared_ptr`中。
使用`std::make_shared`可以避免這個問題。調用的代碼看起來如下所示:
```cpp
processWidget(std::make_shared<Widget>(),computePriority);//不會有內存泄漏的危險
```
在runtime的時候,`std::make_shared`或者computePriority都有可能被第一次調用。如果是`std::make_shared`先被調用,被動態分配的Widget安全的存儲在返回的`std::shared_ptr`中(在computePriority被調用之前)。如果computePriority產生了異常,`std::shared_ptr`的析構函數會負責把它所擁有的Widget回收。如果computePriority首先被調用并且產生出一個異常,`std::make_shared`不會被調用,因此也不必擔心動態分配的Widget會產生泄漏的問題。
如果我們將std::shared_ptr和std::make_shared替換為std::unique_ptr和對應的std::make_unique,同樣的分析也會適用。適用std::make_unique而不使用new的原因和使用std::make_shared的目的相同,都是出于寫出異常安全(exception-safe)代碼的考慮。
一個使用`std::make_shared`(和直接使用new相比)的顯著特性就是提升了效率。使用std::make_shared允許編譯器利用簡潔的數據結構產生出更簡潔,更快的代碼。考慮下面直接使用new的效果
```cpp
std::shared_ptr<Widget> spw(new Widget);
```
很明顯的情況是代碼只需一次內存分配,但實際上它執行了兩次。Item 19解釋了每一個std::shared_ptr都指向了一個包含被指向對象的引用計數的控制塊,控制塊的分配工作在std::shared_ptr的構造函數內部完成。直接使用new,就需要一次為Widget分配內存,第二次需要為控制塊分配內存。
如果使用的是std::make_shared,
```cpp
auto spw = std::make_shared<Widget>();
```
一次分配足夠了。這是因為std::make_shared分配了一整塊空間,包含了Widget對象和控制塊。這個優化減少了程序的靜態大小,因為代碼中只包含了一次分配調用,并且加快了代碼的執行速度,因為內存只被分配一次。此外,使用std::make_shared避免了在控制塊中額外添加的一些記錄信息的需要,潛在的減少了程序所需的總內存消耗。
上文的`std::make_shared`效率分析同樣使用于std::allocate_shared,所以std::make_shared的性能優點也可以延伸到std::allocate_shared函數。
上面說了這么多偏愛make函數,而不是直接用new的理由,每一個都理直氣壯。但是,拋開什么軟件工程,異常安全和性能的優點,這個Item教程的目的是偏向make函數,并不是要我們完全依賴它們。這是因為有一些情況下,make函數不能或者不應該被使用。
例如,make函數都不支持指定自定義的deleter(請看Item18和Item19).但是std::unique_ptr以及std::shared_ptr都有構造函數來支持這樣做。比如,給定一個Widget的自定義deleter
```cpp
auto widgetDeleter = [](Widget* pw){...};
```
直接使用new創建一個智能指針來直接使用它
```cpp
std::unique_ptr<Widget, decltype(widgetDeleter)> upw(new Widget, widgetDeleter);
std::shared_ptr<Widget> spw(new Widget, widgetDeleter);
```
用make函數可做不了這種事情。
make函數的第二個限制來自于它們實現的句法細節。Item 7解釋了當創建了一個對象,該對象的類型重載了是否以std::initializer_list為參數的兩種構造函數,使用大括號的方式來構造對象偏向于使用以std::initializer_list為參數的構造函數。而使用括號來構造對象偏向于調用非std::initializer_list的構造函數。make函數完美轉發它的參數給對象的構造函數,但是,它使用的是括號還是大括號方式呢?對于某些類型,這個問題的答案產生的結果大有不同。舉個例子,在下面的調用中:
```cpp
auto upv = std::make_unique<std::vector<int>>(10,20)
auto spv = std::make_shared<std::vector<int>>(10,20);
```
產生的智能指針所指向的std::vector是擁有10個元素,每個元素的值都是20,還是擁有兩個值,分別是10和20?或者說結果是不確定性的?
好消息是結果是確定性的:兩個調用都產生了同樣的std::vector:擁有10個元素,每個元素的值被設置成了20.這就意味著在make函數中,完美轉發使用的是括號而非大括號格式。壞消息是如果你想要使用大括號格式來構造指向的對象,你必須直接使用new.使用make函數需要完美轉發大括號initializer的能力,但是,正如Item 30所說的那樣,大括號initializer是沒有辦法完美轉發的。但是,Item 30同時描述了一個變通方案:使用auto類型推導從大括號initializer(請看Item 2)中來創建一個std::initializer_list對象,然后將auto創建出來的對象傳遞給make函數:
```cpp
//使用std::initializer_list創建
auto initList = {10, 20};
//使用std::initializer_list為參數的構造函數來創建std::vector
auto spv = std::make_shared<std::vector<int>>(initList);
```
對于std::unique_ptr,這里只是存在兩個場景(自定義的deleter以及大括號initializer)make函數不適用。但對于std::shared_ptr來說,問題可不止兩個了。還有另外兩個,但是都可稱之為邊緣情況,但確實有些程序員會處于這種邊緣情況,你也有可能會碰到。
一些對象定義它們自己的new和deleter操作符。這些函數的存在暗示了為這種類型的對象準備的全局的內存分配和回收方法不再適用。通常情況下,這種自定義的new和delete都被設計為只分配或銷毀恰好是一個屬于該類的對象大小的內存,例如,Widget的new和deleter操作符經常被設計為:只是處理大小就是sizeof(Widget)的內存塊的分配和回收。而std::shared_ptr支持的自定義的分配(通過std::allocate_shared)以及回收(通過自定義的deleter)的特性,上文描述的過程就支持的不好了,因為std::allocate_shared所分配的內存大小不僅僅是動態分配對象的大小,它所分配的大小等于對象的大小加上一個控制塊的大小。所以,使用make函數創建的對象類型如果包含了此類版本的new以及delete操作符,此時(使用make)確實是個壞主意。
使用std::make_shared相對于直接使用new的大小及性能優點源自于:std::shared_ptr的控制塊是和被管理的對象放在同一個內存區塊中。當該對象的引用計數變成了0,該對象被銷毀(析構函數被調用)。但是,它所占用的內存直到控制塊被銷毀才能被釋放,因為被動態分配的內存塊同時包含了兩者。
我之前提到過,控制塊除了它自己的引用計數,還記錄了一些其它的信息。引用計數記錄了多少個std::shared_ptr引用了當前的控制塊,但控制塊還包含了第二個引用計數,記錄了多少哥std::weak_ptr引用了當前的控制塊。第二個引用計數被稱之為weak count(備注:在實際情況中,weak count不總是和引用控制塊的std::weak_ptr的個數相等,庫的實現往weak count添加了額外的信息來生成更好的代碼(facilitate better code generation).但為了本Item的目的,我們忽略這個事實,假設它們是相等的).當std::weak_ptr檢查它是否過期(請看Item 19)時,它看看它所引用的控制塊中的引用計數(不是weak count)是否是0(即是否還有std::shared_ptr指向被引用的對象,該對象是否因為引用為0被析構),如果是0,std::weak_ptr就過期了,否則反之。
只要有一個std::weak_ptr還引用者控制塊(即,weak count大于0),控制塊就會繼續存在,包含控制塊的內存就不會被回收。被std::shared_ptr的make函數分配的內存,直至指向它的最后一個std::shared_ptr和最后一個std::weak_ptr都被銷毀時,才會得到回收。
當類型的對象很大,而且最后一個std::shared_ptr的析構于最后一個std::weak_ptr析構之間的間隔時間很大時,該對象被析構與它所占用的內存被回收之間也會產生間隔:
```cpp
class ReallyBigType{...};
auto pBigObj = std::make_shared<ReallyBigType>();
//使用std::make_shared來創建了一個很大的對象
... //創建了一些std::shared_ptr和std::weak_ptr來指向那個大對象
... //最后一個指向對象的std::shared_ptr被銷毀了
//但是仍有指向它的std::weak_ptr存在
...//在這段時間內,之前為大對象分配的內存仍未被回收
...//最后一個指向該對象的std::weak_ptr在次被析構了;控制塊和對象的內存也在此釋放
```
如果直接使用了new,一旦指向ReallyBigType的最后一個std::shared_ptr被銷毀,對象所占的內存馬上得到回收.(本質上使用了new,控制塊和動態分配的對象所處的內存不在一起,可以單獨回收)
```cpp
class ReallyBigType{...}; //as before
std::shared_ptr<ReallyBigType> pBigObj(new ReallyBigType);//使用new創建了一個大對象
... //就像之前那樣,創建一些std::shared_ptr和std::weak_ptr指向該對象。
... //最后一個指向對象的std::shared_ptr被銷毀了
//但是仍有指向它的std::weak_ptr存在
//但是該對象的內存在此也會被回收
...//在這段時間內,只有為控制塊分配的內存未被回收
...//最后一個指向該對象的std::weak_ptr在次被析構了;控制塊的內存也在此釋放
```
你發現自己處于一個使用std::make_shared不是很可行甚至是不可能的境地,你想到了之前我們提到的異常安全的問題。實際上直接使用new時,只要保證你在一句代碼中,只做了將new的結果傳遞給一個智能指針的構造函數,沒有做其它事情。這也會阻止編譯器在new的使用和調用用來管理new的對象的智能指針的構造函數之間,插入可能會拋出異常的代碼。
舉個栗子,對于我們之間檢查的那個異常不安全的processWidget函數,我們在之上做個微小的修訂。這次,我們指定一個自定的deleter:
```cpp
void processWidget(std::shared_ptr<Widget> spw,
int priority); //as before
void cusDel(Widget *ptr);//自定義的deleter
```
這里有一個異常不安全的調用方式:
```cpp
processWidget(std::shared_ptr<Widget>(new Widget,cusDel),
computePriority())//as before,可能會造成內存泄露
```
回想:如果computerPriority在"new Widget"之后調用,但是在std::shared_ptr構造函數執行之前,并且如果computePriority拋出了一個異常,那么動態分配的Widget會被泄露。
在此我們使用了自定義的deleter,所以就不能使用std::make_shared了,想要避免這個問題,我們就得把Widget的動態分配以及std::shared_ptr的構造單獨放到一句代碼中,然后以該句代碼得到的std::shared_ptr來調用std::shared_ptr.這就是技術的本質,盡管過會兒你會看到我們對此稍加改進來提升性能。
```cpp
std::shared_ptr<Widget> spw(new Widget, cusDel);
processWidget(spw, computePriority());//正確的,但不是最優的:看下面
```
確實可行,因為即使構造函數拋出異常,std::shared_ptr也已經接收了傳給它的構造函數的原生指針的所有權.在本例中,如果spw的構造函數拋出異常(例如,假如因為無力去給控制塊動態分配內存),它依然可以保證cusDel可以在“new Widget”產生的指針上面調用。
在異常非安全的調用中,我們傳遞了一個右值給processWidget,
```cpp
processWidget(std::shared_ptr<Widget>(new Widget, cusDel), //arg是一個右值
computePriority());
```
而在異常安全的調用中,我們傳遞了一個左值:
```cpp
processWidget(spw, computePriority());//arg是一個左值
```
這就是造成性能問題的原因。
因為processWidget的std::shared_ptr參數按值傳遞,從右值構造只需要一個move,然而從左值構造卻需要一個copy操作。對于std::shared_ptr來說,區別是顯著的,因為copy一個std::shared_ptr需要對它的引用計數進行原子加1,然后move一個std::shared_ptr不需要對引用計數做任何操作。對于異常安全的代碼來說,若想獲得和非異常安全代碼一樣的性能表現,我們需要對spw用std::move,把它轉化成一個右值(看Item 23):
```cpp
processWidget(std::move(spw), computePriority());
//即異常安全又獲得了效率
```
是不是很有趣,值得一看。但是這種情況不是很常見。因為你也很少有原因不使用make函數。如果不是非要用其他的方式不可,我還是推薦你盡量使用make函數。
|要記住的東西|
|:--------- |
|和直接使用new相比,使用make函數減少了代碼的重復量,提升了異常安全度,并且,對于std::make_shared以及std::allocate_shared來說,產生的代碼更加簡潔快速|
|也會存在使用make函數不合適的場景:包含指定自定義的deleter,以及傳遞大括號initializer的需要|
|對于std::shared_ptr來說,使用make函數的額外的不使用場景還包含(1)帶有自定義內存管理的class(2)內存非常緊俏的系統,非常大的對象以及比對應的std::shared_ptr活的還要長的std::weak_ptr|
- 出版者的忠告
- 致謝
- 簡介
- 第一章 類型推導
- 條款1:理解模板類型推導
- 條款2:理解auto類型推導
- 條款3:理解decltype
- 條款4:知道如何查看類型推導
- 第二章 auto關鍵字
- 條款5:優先使用auto而非顯式類型聲明
- 條款6:當auto推導出非預期類型時應當使用顯式的類型初始化
- 第三章 使用現代C++
- 條款7:創建對象時區分()和{}
- 條款8:優先使用nullptr而不是0或者NULL
- 條款9:優先使用聲明別名而不是typedef
- 條款10:優先使用作用域限制的enmu而不是無作用域的enum
- 條款11:優先使用delete關鍵字刪除函數而不是private卻又不實現的函數
- 條款12:使用override關鍵字聲明覆蓋的函數
- 條款13:優先使用const_iterator而不是iterator
- 條款14:使用noexcept修飾不想拋出異常的函數
- 條款15:盡可能的使用constexpr
- 條款16:保證const成員函數線程安全
- 條款17:理解特殊成員函數的生成
- 第四章 智能指針
- 條款18:使用std::unique_ptr管理獨占資源
- 條款19:使用std::shared_ptr管理共享資源
- 條款20:在std::shared_ptr類似指針可以懸掛時使用std::weak_ptr
- 條款21:優先使用std::make_unique和std::make_shared而不是直接使用new
- 條款22:當使用Pimpl的時候在實現文件中定義特殊的成員函數
- 第五章 右值引用、移動語義和完美轉發
- 條款23:理解std::move和std::forward
- 條款24:區分通用引用和右值引用
- 條款25:在右值引用上使用std::move 在通用引用上使用std::forward
- 條款26:避免在通用引用上重定義函數
- 條款27:熟悉通用引用上重定義函數的其他選擇
- 條款28:理解引用折疊
- 條款29:假定移動操作不存在,不廉價,不使用
- 條款30:熟悉完美轉發和失敗的情況
- 第六章 Lambda表達式
- 條款31:避免默認的參數捕捉
- 條款32:使用init捕捉來移動對象到閉包
- 條款33:在auto&&參數上使用decltype當std::forward auto&&參數
- 條款34:優先使用lambda而不是std::bind
- 第七章 并發API
- 條款35:優先使用task-based而不是thread-based
- 條款36:當異步是必要的時聲明std::launch::async
- 條款37:使得std::thread在所有的路徑下無法join
- 條款38:注意線程句柄析構的行為
- 條款39:考慮在一次性事件通信上void的特性
- 條款40:在并發時使用std::atomic 在特殊內存上使用volatile
- 第八章 改進
- 條款41:考慮對拷貝參數按值傳遞移動廉價,那就盡量拷貝
- 條款42:考慮使用emplace代替insert