std::make_unique<T>和std::make_shared<T>
更建議使用:std::make_unique<T>構造unique_ptr對象;std::make_shared<T>構造shared_ptr對象
?
std::make_shared是C++11的一部分,std::make_unique不是,它在C++14才納入標準庫。如果你使用的是C++11,不用憂傷,因為std::make_unique的簡單版本很容易寫出來:
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。這種形式的函數不支持數組和自定義刪除器。
三個make函數:std::make_unique、std::make_shared、std::allocate_shared,make函數:把任意集合的參數完美轉發給動態分配對象的構造函數,然后返回一個指向那對象的智能指針。std::allocate_shared,它與std::make_shared類似,除了它第一個參數是個分配器,指定動態分配對象的方式。
使用make函數更可取的第一個原因。考慮以下:
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函數
它們本質上的不同是:使用new的版本重復著需要創建的類型(即出現了兩次Widget),而使用make函數不需要。
第二個原因異常安全。
void processWidget(std::shared_ptr<Widget> spw, int priority);計算優先級的函數,
int computePriority();processWidget(std::shared_ptr<Widget>(new Widget), computePriority()); // 可能會資源泄漏
這代碼中new出來的Widget可能會泄漏,為什么?
調用processWidget時,下面的事會在processWidget開始前執行:
- “new Widget”。
- std::shared_ptr構造函數執行。
- computePriority運行。
編譯器在生成代碼時不會保證上面的執行順序,“new Widget”一定會在std::shared_ptr構造函數之前執行,但是computePriority可能在它們之前就被調用了,可能在它們之后,可能在它們之間。所以,編譯器生成代碼的執行順序有可能是這樣的:
如果生成的代碼真的是這樣,那么在運行時,computePriority產生了異常,步驟1中動態分配的Widget就泄漏了
使用std::make_shared可以避免這問題。
processWidget(std::make_shared<Widget>(), computePriority())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::unique_ptr和std::shared_ptr可以指定刪除器,make函數不可以,
auto widgetDeleter = [](Widget* pw) {...}我們可以直接使用new創建智能指針:
?std::unique_ptr<Widget, decltype(widgetDeleter)> upw(new Widget, widgetDeleter);
std::shared_ptr<Widget> spw(new Widget, widgetDeleter);
make函數的第二個限制。當創建一個對象時,如果該對象的重載構造函數帶有std::initializer_list參數,那么使用大括號創建對象會偏向于使用帶std::initializer_list構造,要使用圓括號創建對象才能使用到非std::initializer_list構造。make函數把它們的參數完美轉發給對象的構造函數,那么它們用的是大括號還是圓括號呢?
?auto upv = std::make_unique<std::vector<int>>(10, 20);
auto spv = std::make_shared<std::vector<int>>(10, 20);
上面兩個都創建內含10個值為20的std::vector。make函數內,完美轉發使用的是圓括號,而不是大括號。壞消息是如果你想用大括號初始化來構造指向的對象,你只能直接使用new,如果你想使用make函數,就要求完美轉發的能力支持大括號初始化,但是大括號初始化不能被完美轉發。不過也有一種能工作的方法:用auto推斷大括號,從而創建一個std::initializer_list對象,然后把auto變量傳遞給make函數:
?// 創建 std::initializer_list
auto initList = {10, 20};
// 使用std::initializer_list構造函數創建std::vector,容器中只有兩個元素
auto spv = std::make_shared<std::vector<int>>(initList);
對于std::unique_ptr,只有兩種情況(自定義刪除器和大括號初始化)會讓它的make函數出問題。對于std::shared_ptr和它的make函數,就多兩種情況,這兩種情況都是邊緣情況,不過一些開發者就喜歡住在邊緣。
一些類定義了自己的operator new和operator delete函數,這些函數的出現暗示著常規的全局內存分配和回收不適合這種類型的對象。通常情況下,設計這些函數只有為了精確分配和銷毀對象。這兩個函數不適合std::shared_ptr的自定義分配(借助std::allocate_shared)和回收(借助自定義刪除器),因為std::allocate_shared請求內存的大小不是對象的尺寸,而是對象尺寸加上控制塊尺寸。結果就是,使用make函數為那些定義自己版本的operator new和operator delete的類創建對象是糟糕的。
比起直接使用new,std::make_shared的占用內存大小和速度優勢來源于:std::shared_ptr的控制塊與它管理的對象放在同一塊內存。當引用計數為0時,對象被銷毀(即調用了析構函數),但是,它使用的內存不會釋放,除非控制塊也被銷毀,因為對象和控制塊在同一塊動態分配的內存上。
控制塊上除了引用計數還有別的信息。引用計數記錄的是有多少std::shared_ptr指向控制塊,但是控制塊還有第二種引用計數,記錄有多少std::weak_ptr指向控制塊。這種引用計數稱為weak count。當std::weak_ptr檢查它是否過期時(expired),它通過檢查控制塊中的引用計數(不是weak count)來實現。如果引用計數為0,std::weak_ptr就過期,否則就沒有過期。
但是,只要有std::weak_ptr指向控制塊(weak count大于0),控制塊就必須繼續存在,而只要控制塊存在,容納它的內存塊也依舊存在。那么,通過make函數創建對象分配的內存,要直到最后一個指向它的std::shared_ptr和std::weak_ptr對象銷毀,才能被回收。
如果對象的類型非常大,并且最后一個std::shared_ptr銷毀和最后一個std::weak_ptr銷毀之間的時間間隔很大,那么對象銷毀和內存被回收之間的會有延遲
如果直接使用new,ReallyBigType對象的內存只要最后一個std::shared_ptr被銷毀就能被釋放。
有個小小的性能問題,在異常不安全的調用中,我們傳給processWidget的是一個右值
?processWidget(
std::shared_ptr<Widget>(new Widget, cusDel), // 參數是右值
computePriority()
);
但是在異常安全的調用中,我們傳遞的是個左值:
processWidget(spw, computePriority()); // 參數是左值因為processWidget的std::shared_ptr參數是值傳遞,從一個右值構造使用的是移動,從一個左值構造使用的是拷貝。對于std::shared_ptr,這差別挺大的,因為拷貝一個std::shared_ptr需要增加它的引用計數,而移動操作完全不用操作引用計數。
使用std::move來把spw轉化為右值:
processWidget(std::move(spw), computePriority()); // 現在也一樣高效這是有趣的而且值得知道,但是通常也是不相干的,因為你很少有理由不用make函數,除非你有迫不得已的理由,否則,你應該使用make函數。
總結
需要記住的3點:
- 相比于直接使用new,make函數可以消除代碼重復,提高異常安全,而且std::make_shared和std::allocate_shared生成的代碼更小更快。
- 不適合使用make函數的場合包括需要指定自定義刪除器和想要傳遞大括號初始值。
- 對于std::shared_ptr,使用make函數可能是不明智的場合包括(1)自定義內存管理函數的類、(2)內存緊張的系統中,有非常大的對象,然后std::weak_ptr比std::shared_ptr長壽。
總結
以上是生活随笔為你收集整理的std::make_unique<T>和std::make_shared<T>的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一个单向链表,输出该链表中倒数第k个结点
- 下一篇: 螺旋模型(Spiral Model)