天天看點

【C++11】make_shared函數|std::make_unique

make_shared的使用:

shared_ptr<string> p1 = make_shared<string>(10, '9');  

shared_ptr<string> p2 = make_shared<string>("hello");  

shared_ptr<string> p3 = make_shared<string>(); 
      

好處:減少配置設定次數

std::shared_ptr<Widget> spw(new Widget);  配置設定2次記憶體

auto spw = std::make_shared<Widget>(); 隻配置設定1次記憶體

盡量使用make_shared初始化

C++11 中引入了智能指針, 同時還有一個模闆函數 std::make_shared 可以傳回一個指定類型的 std::shared_ptr, 那與 std::shared_ptr 的構造函數相比它能給我們帶來什麼好處呢 ?

make_shared初始化的優點

1、提高性能

shared_ptr 需要維護引用計數的資訊:

強引用, 用來記錄目前有多少個存活的 shared_ptrs 正持有該對象. 共享的對象會在最後一個強引用離開的時候銷毀( 也可能釋放).

弱引用, 用來記錄目前有多少個正在觀察該對象的 weak_ptrs. 當最後一個弱引用離開的時候, 共享的内部資訊控制塊會被銷毀和釋放 (共享的對象也會被釋放, 如果還沒有釋放的話).

如果你通過使用原始的 new 表達式配置設定對象, 然後傳遞給 shared_ptr (也就是使用 shared_ptr 的構造函數) 的話, shared_ptr 的實作沒有辦法選擇, 而隻能單獨的配置設定控制塊:

【C++11】make_shared函數|std::make_unique

如果選擇使用 make_shared 的話, 情況就會變成下面這樣:

【C++11】make_shared函數|std::make_unique

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::make_shared的效率分析可以同樣地應用在std::allocate_shared上,是以std::make_shared的性能優點也可以擴充到這個函數上。

對std::make_shared的性能分析同樣适用于std::allocated_shared,是以std::make_shared的性能優勢也同樣存在于std::allocated_shared。

2、 異常安全

我們在調用processWidget的時候使用computePriority(),并且用new而不是std::make_shared:

processWidget(std::shared_ptr<Widget>(new Widget),  //潛在的資源洩露 
computePriority());
      

就像注釋訓示的那樣,上面的代碼會導緻new創造出來的Widget發生洩露。那麼到底是怎麼洩露的呢?調用代碼和被調用函數都用到了std::shared_ptr,并且std::shared_ptr就是被設計來阻止資源洩露的。當最後一個指向這兒的std::shared_ptr消失時,它們會自動銷毀它們指向的資源。如果每個人在每個地方都使用std::shared_ptr,那麼這段代碼是怎麼導緻資源洩露的呢?

答案和編譯器的翻譯有關,編譯器把源代碼翻譯到目标代碼,在運作期,函數的參數必須在函數被調用前被估值,是以在調用processWidget時,下面的事情肯定發生在processWidget能開始執行之前:

表達式“new Widget”必須被估值,也就是,一個Widget必須被建立在堆上。

std::shared_ptr(負責管理由new建立的指針)的構造函數必須被執行。

computePriority必須跑完。

編譯器不需要必須産生這樣順序的代碼。但“new Widget”必須在std::shared_ptr的構造函數被調用前執行,因為new的結構被用為構造函數的參數,但是computePriority可能在這兩個調用前(後,或很奇怪地,中間)被執行。也就是,編譯器可能産生出這樣順序的代碼:

執行“new Widget”。
執行computePriority。
執行std::shared_ptr的構造函數。
      

如果這樣的代碼被産生出來,并且在運作期,computePriority産生了一個異常,則在第一步動态配置設定的Widget就會洩露了,因為它永遠不會被存放到在第三步才開始管理它的std::shared_ptr中。

使用std::make_shared可以避免這樣的問題。調用代碼将看起來像這樣:

processWidget(std::make_shared<Widget>(),       //沒有資源洩露
computePriority());           
      

在運作期,不管std::make_shared或computePriority哪一個先被調用。如果std::make_shared先被調用,則在computePriority調用前,指向動态配置設定出來的Widget的原始指針能安全地被存放到被傳回的std::shared_ptr中。如果computePriority之後産生一個異常,std::shared_ptr的析構函數将發現它持有的Widget需要被銷毀。并且如果computePriority先被調用并産生一個異常,std::make_shared就不會被調用,是以這裡就不需要考慮動态配置設定的Widget了。

如果使用std::unique_ptr和std::make_unique來替換std::shared_ptr和std::make_shared,事實上,會用到同樣的理由。是以,使用std::make_unique代替new就和“使用std::make_shared來寫出異常安全的代碼”一樣重要。

缺點

構造函數是保護或私有時,無法使用 make_shared

​make_shared​

​ 雖好, 但也存在一些問題, 比如, 當我想要建立的對象沒有公有的構造函數時, ​

​make_shared​

​ 就無法使用了, 當然我們可以使用一些小技巧來解決這個問題, 比如這裡 ​​How do I call ::std::make_shared on a class with only protected or private constructors?​​

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

​make_shared​

​ 隻配置設定一次記憶體, 這看起來很好. 減少了記憶體配置設定的開銷. 問題來了, ​

​weak_ptr​

​ 會保持控制塊(強引用, 以及弱引用的資訊)的生命周期, 而是以連帶着保持了對象配置設定的記憶體, 隻有最後一個 ​

​weak_ptr​

​ 離開作用域時, 記憶體才會被釋放. 原本強引用減為 0 時就可以釋放的記憶體, 現在變為了強引用, 若引用都減為 0 時才能釋放, 意外的延遲了記憶體釋放的時間. 這對于記憶體要求高的場景來說, 是一個需要注意的問題.

原文:

連結:https://www.jianshu.com/p/03eea8262c11

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);

……

見前面的 《2、 異常安全》

假如我們把std::shared_ptr和std::make_shared替換成std::unique_ptr 和std::make_unique,會發生相同的事情。使用std::make_unique來代替new在寫異常安全的代碼裡是和使用std::make_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);
      

對于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。

繼續閱讀