天天看點

第23課 優先選用make系列函數

一. make系列函數

(一)三個make函數

  1. std::make_shared:用于建立shared_ptr。GCC編譯器中,其内部是通過調用std::allocate_shared來實作的。

  2. std::make_unique:C++14中加入标準庫。

  3. std::allocate_shared:行為和std::make_shared一樣,隻不過第1個實參是個用以動态配置設定記憶體的配置設定器對象。

第23課 優先選用make系列函數
第23課 優先選用make系列函數

//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_shared的實作(GCC編譯器)
template<typename _Tp, typename... _Args>
inline shared_ptr<_Tp> make_shared(_Args&&... __args)
{
    typedef typename std::remove_const<_Tp>::type _Tp_nc;
    return std::allocate_shared<_Tp>(std::allocator<_Tp_nc>(),
                   std::forward<_Args>(__args)...);
}      

std::make_unique和std::make_shared的實作

(二)與new相比,make系列函數的優勢

  1. 避免代碼備援:建立智能指針時,被建立對象的類型隻需寫1次。如make_shared<T>(),而用new建立智能指針時,需要寫2次。

  2. 異常安全:make系列函數可編寫異常安全代碼,改進了new的異常安全性。

  3. 提升性能:編譯器有機會利用更簡潔的資料結構産生更小更快的代碼。使用make_shared<T>時會一次性進行記憶體配置設定,該記憶體單塊(single chunck)既儲存了T對象又儲存與其相關聯的控制塊。而直接使用new表達式,除了為T配置設定一次記憶體,還要為與其關聯的控制塊再進行一次記憶體配置設定。 

第23課 優先選用make系列函數

二. make系列函數的局限

(一)所有的make系列函數都不允許自定義删除器。

(二)make系列函數建立對象時,不能接受{}初始化清單。(這是因為完美轉發的轉發函數是個模闆函數,它利用模闆類型進行推導。是以無法将“{}”推導為initializer_list,具體見《完美轉發》一課)。換言之,make系列隻能将圓括号内的形參完美轉發。

(三)自定義記憶體管理的類(如重載了operator new 和operator delete),不建議使用make_shared來建立。原因如下:

  1. 重載operator new和operator delete時,往往用來配置設定和釋放該類精确尺寸(sizeof(T))的記憶體塊。

  2. 而make_shared建立的shared_ptr,是一個自定義了配置設定器(std::allocate_shared)和删除器的智能指針,由allocate_shared配置設定的記憶體大小也不等于上述的尺寸,而是在此基礎上加上控制塊的大小。

  3. 是以,不建議使用make函數為那些重載了operator new和operator delete的類建立對象。

(四)對象的記憶體可能無法及時回收

  1. make_shared 隻配置設定一次記憶體,減少了記憶體配置設定的開銷。使得控制塊和托管對象在同一記憶體塊上配置設定。而控制塊是由shared_ptr和weak_ptr共享的,是以兩者共同管理着這個記憶體塊(托管對象+控制塊)。

#include <iostream>
#include <memory> //for smart pointer
#include <vector>

using namespace std;

class Widget
{
public:
    Widget(){}
    Widget(int x, int y){ cout << "Widget(int x, int y)" << endl; }
    Widget(const std::initializer_list<int> li) { cout << "Widget(std::initializer_list<int> li)"<< endl; }
};

void processWidget(std::shared_ptr<Widget> spw, int priority){}
int computePriority() { /*throw 1;*/ return 0; }//假設該函數會抛出異常

class ReallyBigType {};//大對象

int main()
{
    //1. make系列函數的優勢
    //1.1 避免代碼備援,減少重複書寫類型
    auto upw1(std::make_unique<Widget>());    //使用make系列函數,Widget隻需寫一次
    std::unique_ptr<Widget> upw2(new Widget); //使用new,Widget需寫二次。

    //1.2 make系統異常安全性更高
    //在将實參傳遞processWidget前,各個參數時必須先被計算出來,假設順序如下(因編譯器和調用約定而異)
    //A. 先new Widget,即一個Widget對象在堆上建立。
    //B. 執行computePriority,但假設此時該函數産生異常,那上面的堆對象就會洩漏。
    //C. 正常流程下,應執行shared_ptr構造函數,但由于第2步的異常,使得第1步配置設定的堆對象永遠不會被這個
    //   shared_ptr接管(實際上該shared_ptr自己都沒有機會建立),于是資源洩漏!
    processWidget(shared_ptr<Widget>(new Widget), computePriority());//潛在資源洩漏!
    
    //異常安全!
    processWidget(make_shared<Widget>(), computePriority()); //如果make_shared首先被調用當computePriority
                                                             //發生異常時,則之前的shared_ptr會被釋放,
                                                             //進而釋放Widget對象。如果computePriority先
                                                             //調用,則make_shared沒有機會被調用,也就不會
                                                             //有資源洩漏!
    //1.3 make_shared一次性配置設定記憶體
    auto spw1 = std::make_shared<Widget>(); //一次性配置設定一個記憶體單塊,可容納Widget對象和控制塊記憶體
    std::shared_ptr<Widget> spw2(new Widget); //2次配置設定:new和配置設定控制塊各一次。

    //2. make系列函數的局限性
    //2.1 make不能自定義删除器
    auto widgetDeleter = [](Widget* pw) {delete pw; };
    std::unique_ptr<Widget, decltype(widgetDeleter)> upw3(new Widget, widgetDeleter);
    std::shared_ptr<Widget> spw3(new Widget, widgetDeleter);

    //2.2 make系列函數不能接受{}初始化
    auto upv = std::make_unique<std::vector<int>>(10, 20); //10個元素,每個都是20。而不是隻有兩個元素
    auto spv = std::make_shared<std::vector<int>>(10, 20); //同上

    auto pw1 = new Widget(10, 20);   //使用圓括号,比對Widget(int x, int y)
    auto pw2 = new Widget{ 10, 20 }; //使用大括号,比對Widget(initializer_list)
    delete pw1;
    delete pw2;

    auto spw = std::make_shared<Widget>(10, 20); //使用圓括号,比對Widget中非initializer_list形參的構造函數
    //auto spw = std::make_shared<Widget>({10,20}); //error,make無法轉發大括号初始化清單(原因見《完美轉發》一課)
    auto initList = { 10, 20 }; //initList推導為initializer_list<int>
    auto splst = std::make_shared<Widget>(initList); //ok,比對Widget(const std::initializer_list<int> li) 

    //2.3 對象的記憶體可能無法及時回收
    auto pBigObj = std::make_shared<ReallyBigType>(); //通過make_shared建立大對象
    //...   //建立指向大對象的多個std::shared_ptr和std::weak_ptr,并使用這些智能指針來操作對象
    //...   //最後一個指向大對象的shard_ptr在此析構,但若幹weak_ptr仍然存在
    //...   //此時,記憶體塊隻析構,還沒回收。因為weak_ptr還共享着記憶體塊中的控制塊
    //...   //最後一個指向大對象的weak_ptr析構,記憶體塊(托管對象+控制塊)才被回收。由于weak_ptr的生命期比shared_ptr長,
            //出現了記憶體塊延遲回收的現象。

    //使用new方法則不會出現上述現象
    shared_ptr<ReallyBigType> pBigObj2(new ReallyBigType); //通過new,而不是make_shared建立
    //...   //同前,建立指向多個指向大對象的shared_ptr和weak_ptr。
    //...   //最後一個指向大對象的shard_ptr在此析構,但若幹weak_ptr仍然存在。此時大對象的記憶體由于強引用為0,被回收。
    //...   //此階段,僅控制塊占用的記憶體處于未回收狀态。
    //...   //最後一個指向該對象的weak_ptr析構,控制塊被回收。

    return 0;
}