c++ 之智能指針:盡量使用std::make_unique和std::make_shared而不直接使用new


轉載自https://blog.csdn.net/p942005405/article/details/84635673

關於make_unique的構造及使用例程,MSDN的講解非常詳細 (https://msdn.microsoft.com/zh-cn/library/dn439780.aspx )

使用過程中,主要有這么幾個關鍵點:

1.  make_unique 同 unique_ptr 、auto_ptr 等一樣,都是 smart pointer,可以取代new 並且無需 delete pointer,有助於代碼管理。

2. make_unique 創建並返回 unique_ptr 至指定類型的對象,這一點從其構造函數能看出來。make_unique相較於unique_ptr 則更加安全。

3. 編譯器不同,make_unique 要求更新(Visual Studio 2015)。

以下是關於 make_unique 與make_shared 的知識介紹 :
 

條款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,也別擔心,你很容易寫出一個基本的版本。看這里:

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完美傳遞了參數給對象的構造函數,從一個原始指針構造出一個std::unique_ptr,返回創建的std::unique_ptr。這個形式的函數不支持數組和定制刪除器(見條款18),但它證明了一點點的努力就可以根據需要創建一個make_unique。要記住的是不要把你的版本放到std命名空間里,因為你不想當升級到c++14后會和庫提供的標准實現沖突吧。

        std::make_unique 和 std::make_shared是三個make函數中的兩個,make函數用來把一個任意參數的集合完美轉移給一個構造函數從而生成動態分配內存的對象,並返回一個指向那個對象的靈巧指針。第三個make是std::allocate_shared。它像std::make_shared一樣,除了第一個參數是一個分配器對象,用來進行動態內存分配。

        優先使用make函數的第一個原因即使用最簡單的構造靈巧指針也能看出來。考慮如下代碼:

auto upw1(std::make_unique<Widget>());     // with make func
std::unique_ptr<Widget> upw2(new Widget); // without make func


auto spw1(std::make_shared<Widget>());     // with make func
std::shared_ptr<Widget> spw2(new Widget); // without make func

        我標注了基本的區別:

        使用new的版本重復了被創建對象的鍵入,但是make函數則沒有。重復類型違背了軟件工程的一個重要原則:應該避免代碼重復,代碼中的重復會引起編譯次數增加,導致目標代碼膨脹,最終產生更難以維護的代碼,通常會引起代碼不一致,而不一致經常導致bug產生。另外,輸入兩次比輸入一次要費力些,誰都想減少敲鍵盤的負擔。

        優先使用make函數的第二個原因是和異常安全有關。假設我們有個函數來根據一些優先級處理一個Widget對象:

void processWidget(std::shared_ptr<Widget> spw, int priority);

        對std::shared_ptr進行傳值看上去有些疑問,但是條款41解釋了如果processWidget始終構造一個std::shared_ptr的拷貝(比如保存在一個數據結構里,該數據結構跟蹤已經被處理過的Widget對象),這可能是個合理的選擇。

        現在我們假設有個函數來計算相對優先級

int computePriority();

        我們在一個調用processWidget時使用new而不使用std::make_shared:

processWidget(std::shared_ptr<Widget>(new Widget), // potential
                         computePriority());       // resource
                                                   // leak!

        就像注釋說明的,這段代碼可能會因為new而引起內存泄漏。但怎么引起的呢?調用代碼和被調函數都是在使用std::shared_ptr,這本來就是設計成避免內存泄漏的。它可以保證在對象不被使用時銷毀所指的對象。只要一直都在使用std::shared_ptr,怎么會泄漏呢?

        答案是和編譯器翻譯代碼到目標代碼有關。在運行期,傳遞給函數的參數必須先計算,然后才發生函數調用。因此在調用processWidget時,下面的事情必須在processWidget開始執行前發生:

1.表達式"new Widget"必須先計算,一個Widget對象必須先創建在堆上;

2.負責new出來的對象指針的std::shared_ptr<Widget>的構造函數必須執行;

3.computePriority必須運行。

 

        沒有人要求編譯器必須按這樣的順序來生成代碼。“new Widget”必須在 std::shared_ptr構造函數前被調用,因為new的結果用作構造函數的參數,但是computePriority可以在上述調用前、后或者中間執行。也就是說編譯器可能會產生這樣的代碼來按如下順序執行:

1.執行“new Widget”;

2.執行computePriority;

3.調用std::shared_ptr構造函數。

        假如這樣的代碼生成,在運行期,computePriority產生了異常,那么第一步生成的對象會被泄漏掉,因為沒有在第3步被保存到 std::shared_ptr。

        使用std::make_shared可以避免這個問題。調用代碼如下:

processWidget(std::make_shared<Widget>(),       // no potential
                         computePriority());    // resource leak

        在運行期,std::make_shared或者computePriority被首先調用。如果是std::make_shared被首先調用,指向動態內存對象的原始指針會被安全的保存在返回的std::shared_ptr對象中,然后是computePriority被調用 。如果computePriority產生了異常,那std::shared_ptr析構會知道於是它所擁有的對象會被銷毀。如果computePriority先調用並產生了異常,std::make_shared不會被調用,因此也不會有動態分配的內存擔心。

        假如我們把std::shared_ptr和std::make_shared替換成std::unique_ptr 和std::make_unique,會發生相同的事情。使用std::make_unique來代替new在寫異常安全的代碼里是和使用std::make_shared一樣重要。

        另一個使用std::make_shared的優勢(和直接使用new相比)是會提升性能。使用std::make_shared會讓編譯器產生更小更快的代碼,從而產生更簡潔的數據結構。考慮如下直接使用new的代碼:

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

        很顯然這段代碼會引起一個內存分配,但實際上是有兩次內存分配。條款19解釋了每個std::shared_ptr會指向一個控制塊,這個控制塊除了其他一些東西,包含了所指對象的引用計數。控制塊的內存分配是std::shared_ptr的構造函數匯總進行的。這樣直接用new需要為Widget來分配一次內存,還要為控制塊再分配一次內存。

        假如用std::make_shared來代替new,

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

一次內存分配就足夠了。那是因為std::make_shared會分配一塊獨立的內存既保存Widget對象又保存控制塊。這個優化減小了程序的靜態尺寸,因為代碼只包含一次內存分配的調用,同時增加了代碼執行速度,因為只有一次內存分配。另外,使用std::make_shared會避免控制塊中的一些記錄信息,潛在的減少了程序中內存的使用。

        對std::make_shared的性能分析同樣適用於std::allocated_shared,因此std::make_shared的性能優勢也同樣存在於std::allocated_shared。

        make函數的參數相對直接使用new來說也更健壯。盡管有如此多的工程特性、異常安全以及效率優勢,我們這個條款是“盡量”使用make函數,而沒有說排除其他情況。那是因為還有情況不能或者不應該使用make函數。

        比如,make函數都不允許使用定制刪除器(見條款18,條款19),但是std::unique_ptr和std::shared_ptr的構造函數都可以給Widget對象一個定制刪除器。

auto widgetDeleter = [](Widget* pw) { … };

        直接使用new來構造一個有定制刪除器的靈巧指針:

std::unique_ptr<Widget, decltype(widgetDeleter)>
    upw(new Widget, widgetDeleter);
 

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

用make函數沒法做到這一點。

        make函數的第二個限制是無法從實現中獲得句法細節。條款7解釋了當創建一個對象時,如果其類型通過std::initializer_list參數列表來重載構造函數的,盡量用大括號來創建對象而不是std::initializer_list構造函數。相反,用圓括號創建對象時,會調用non-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,還是指向2個元素的數組其值分別是10和20 ?或者無限制?

 

        好消息是並非無限制的 :兩個調用都是構造了10元素的數組,每個元素值都是20。說明在make函數里,轉移參數的代碼使用了圓括號,而不是大括號。壞消息是,假如你想使用大括號初始化器( braced initializer)來創建自己的指向對象的指針,你必須直接使用new。使用make函數需要能夠完美傳遞一個大括號初始化器的能力,但是,如條款30中所說的,大括號初始化器不能夠完美傳遞。但條款30也給出了一個補救方案:從大括號初始化器根據auto類型推導來創建一個 std::initializer_list對象,然后把auto對象傳遞給make函數:

// create std::initializer_list
auto initList = { 10, 20 };
 

// create std::vector using std::initializer_list ctor
auto spv = std::make_shared<std::vector<int>>(initList);

 轉自:https://blog.csdn.net/coolmeme/article/details/43405155 

        對於std::unique_ptr來說,其make函數就只在這兩種場景(定制刪除器和大括號初始化器)有問題。對於std::shared_pr來說,其make函數的問題會更多一些。這兩種都是邊緣情況,但是一些開發者就喜歡處理邊緣情況,你也許也是其中之一。

        一些類會定義自己的opeator new和operator delete。這表示全局的內存分配和釋放函數對該對象不合適。通常情況下,類特定的這兩個函數被設計成精確的分配或釋放類大小的內存塊,比如,類Widget的operator new和operator delete僅僅處理sizeof(Widget)大小的內存塊。這兩個函數作為定制的分配器(通過std::allocate_shared)和解析器(通過定制解析器),對std::shared_ptr的支持並不是很好的選擇。因為std::allocate_shared需要的內存數量並不是動態分配的對象的大小,而是對象的大小加上控制塊的大小。因此,對於某些對象,其類有特定的operate new和operator delete,使用make函數去創建並不是很好的選擇。

        std::make_shared在尺寸和速度上的優點同直接使用new相比,阻止了std::shared_ptr的控制塊作為管理對象在同樣的內存塊上分配。當對象的引用計數變為0,對象被銷毀(析構函數被調)。然而,直到控制塊同樣也被銷毀,它所擁有的內存才被釋放,因為兩者都在同一塊動態分配的內存上。

        我前面提到過,控制塊除了引用計數本身還包含了其他一些信息。引用計數記錄了有多少std::shared_ptr指針指向控制塊。另外控制塊中還包含了第二個引用計數,記錄了有多少個std::weak_ptr指針指向控制塊。這第二個引用計數被稱作weak count。當一個std::weak_ptr檢查是否過期時(見條款19),它會檢查控制塊里的引用計數(並不是weak count)。假如引用計數為0(假如被指對象沒有std::shared_ptr指向了從而已經被銷毀),則過期,否則就沒過期。

        只要有std::weak_ptr指向一個控制塊(weak count大於0),那控制塊就一定存在。只要控制塊存在,包含它的內存必定存在。這樣通過std::shared_ptr的make函數分配的函數則在最后一個std::shared_ptr和最后一個std::weak_ptr被銷毀前不能被釋放。

        假如對象類型很大,以至於最后一個std::shared_ptr和最后一個std::weak_ptr的銷毀之間的時間不能忽略時,對象的銷毀和內存的釋放間會有個延遲發生。

class ReallyBigType { … };

 

auto pBigObj =  // create very large

     std::make_shared<ReallyBigType>(); // object via
                                        //  std::make_shared


… // create std::shared_ptrs and std::weak_ptrs to

// large object, use them to work with it

… // final std::shared_ptr to object destroyed here,

// but std::weak_ptrs to it remain

… // during this period, memory formerly occupied

// by large object remains allocated

… // final std::weak_ptr to object destroyed here;

// memory for control block and object is released

        當直接使用new時,ReallyBigType對象的內存可以在最后一個std::shared_ptr銷毀時被釋放:

class ReallyBigType { … };       // as before


std::shared_ptr<ReallyBigType> pBigObj(new ReallyBigType);

                                                   // create very large
                                                   // object via new

…    // as before, create std::shared_ptrs and
     // std::weak_ptrs to object, use them with it

…    // final std::shared_ptr to object destroyed here,
     // but std::weak_ptrs to it remain;
     // memory for object is deallocated

…    // during this period, only memory for the
     // control block remains allocated


…    // final std::weak_ptr to object destroyed here;
     // memory for control block is released

        你有沒有發現,你處在一個不可能或者不適合用std::make_shared的情況下,你會確保避免之前我們見到的這類異常安全問題。最好的辦法是確保你直接用new的時候,立即把new的結果傳遞給一個靈巧指針的構造函數,別的什么先不做。這樣會阻止編譯器生成代碼,避免在new和靈巧指針的構造函數(會接管new出來的對象)直接產生異常。

舉個例子,考慮一個對processWidget函數(我們之前測試過)的非異常安全的調用,這次我們定義一個定制刪除器:

void processWidget(std::shared_ptr<Widget> spw, // as before
                                                int priority);
 

void cusDel(Widget *ptr);      // custom
                               // deleter

        這里有個非異常安全的調用:


processWidget(                                           // as before,
            std::shared_ptr<Widget>(new Widget, cusDel), // potential
            computePriority()                            // resource
);                                                       // leak!

 

 

         回憶下:假如computePriority函數在new Widget之后,但是在std::shared_ptr的構造函數之前被調用,如果computePriority拋了異常,那么動態分配的Widget會被泄露。

 

        這里因為使用了定制刪除器,所以不能使用std::make_shared,這里避免問題的方法是把Widget分配內存和構造std::shared_ptr放置到自己的語句中,然后再用std::shared_ptr去調用processWidget。這是這個技巧的本質,當然我們后面會看到我們可以提升其性能:

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


processWidget(spw, computePriority()); // correct, but not
                                       // optimal; see below

        因為std::shared_ptr擁有從構造函數傳遞給它的原始指針,即使在構造函數產生異常時,所以上述代碼運行正常。在這個例子中,如果spw的構造函數拋異常(比如因為不能夠為控制塊分配到動態內存),它仍然會保證調用cusDel去析構new Widget返回的結果。

        不同之處在於,我們在非異常安全的代碼里給processWidget傳遞了一個右值。

processWidget(
    std::shared_ptr<Widget>(new Widget, cusDel), // arg is rvalue
     computePriority()
);

        而在異常安全的調用中,我們傳遞了一個左值

processWidget(spw, computePriority()); // arg is lvalue

        因為processWidget的std::shared_ptr參數是通過傳值的,從一個右值去構造僅僅需要一個move,而從左值去構造需要一個拷貝。對std::shared_ptr來說,這個區別很重要,因為拷貝一個std::shared_ptr需要對其引用計數進行加1的原子操作,而移動一個std::shared_ptr根本不需要對引用計數進行操作。對於這段異常安全的代碼如果要達到非異常安全的代碼的性能,我們在spw上應用std::move,而把它轉化成一個右值(見條款23):

processWidget(std::move(spw),               // both efficient and
                        computePriority()); // exception safe

        這個很有趣,也應該知道。但是同時也無關緊要。因為你應該很少有理由不直接使用make函數。除非你有特別的理由不去用它,否則你應該使用make函數來完成你要做的。

                                                   

需要記住的事情:

1.同直接使用new相比,make函數減小了代碼重復,提高了異常安全,並且對於std::make_shared和std::allcoated_shared,生成的代碼會更小更快。

2.不能使用make函數的情況包括我們需要定制刪除器和期望直接傳遞大括號初始化器。

3.對於std::shared_ptr,額外的不建議使用make函數的情況包括:

  (1)定制內存管理的類,

  (2)關注內存的系統,非常大的對象,以及生存期比 std::shared_ptr長的std::weak_ptr。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM