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