Item 21:優先使用 std::make_unique 與 std::make_shared 而非直接 new (Prefer std::make_unique and std::make_shared to direct use of new)

Overview Table

主題 重點
三個 make functions std::make_unique(C++14)、std::make_shared(C++11)、std::allocate_shared(帶 allocator)
優點 1:消除重複 不必寫兩次型別(auto upw = std::make_unique<Widget>();),減少程式碼重複與不一致
優點 2:例外安全 避免 new 與 smart pointer 建構之間插入其他運算導致的 resource leak
優點 3:效率(僅 make_shared / allocate_shared 一次配置同時容納物件與 control block → 程式更小、更快
不能用 make 的情況(共通) 需要 custom deleter、想用 braced initializer 建構
不建議用 make_shared 的情況 (1) 類別自訂 operator new / operator delete;(2) 記憶體敏感系統 + 超大物件 + std::weak_ptrstd::shared_ptr 活得久
必須直接 new 時 new 的結果立即獨立語句中交給 smart pointer 建構子,再以 std::move 傳遞

為什麼優先用 make functions:型別不重複 + 例外安全

auto upw1make_unique<Widget>();      // 用 make:型別只寫一次
std::unique_ptr<Widget> upw2(new Widget);   // 用 new:Widget 寫了兩次

auto spw1make_shared<Widget>();      // 用 make
std::shared_ptr<Widget> spw2(new Widget);   // 用 new:重複型別

C++11 沒有 std::make_unique,可自行寫一個基本版(勿放進 namespace std,以免升級 C++14 時與標準庫衝突):

template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts&&... params)
{
  // 完美轉發參數給建構子,再包進 unique_ptr 回傳
  return std::unique_ptr<T>(new Tforward<Ts>(params)...);
}
// 此基本版不支援陣列與 custom deleter

例外安全才是關鍵理由。考慮:

void processWidgetshared_ptr<Widget> spw, int priority;
int computePriority();

processWidgetshared_ptr<Widget>(new Widget,  // 潛在
              computePriority());                   // resource leak!

編譯器只保證「new Widget 先於 shared_ptr 建構子」,但 computePriority 可以插在兩者之間執行:

編譯器允許的執行順序(引發洩漏的情境):

  1. new Widget            ← 物件已在 heap 上
  2. computePriority()     ← 若在此拋出例外……
  3. shared_ptr 建構子      ← 永遠不會執行
        │
        └──> Widget 從未被任何 smart pointer 接管 → 記憶體洩漏

使用 make function:

  make_shared<Widget>() ──> 「配置 + 接管」是不可分割的一步
  computePriority()     ──> 無論先後、無論是否拋出例外,都不會洩漏

改用 std::make_shared<Widget>() 後,配置與接管在同一個函式呼叫內完成,任何一側拋出例外都不會產生無人管理的裸指標。同樣的推理完全適用於 std::make_unique

make_shared 的效率優勢:一次配置

建立方式 記憶體配置次數 說明
std::shared_ptr<Widget> spw(new Widget); 2 次 一次給 Widget,一次給 control block
auto spw = std::make_shared<Widget>(); 1 次 單一 chunk 同時容納物件與 control block
new + shared_ptr 建構子:         make_shared:
┌──────────┐   ┌───────────────┐   ┌──────────┬───────────────┐
│  Widget  │   │ control block │   │  Widget  │ control block │
└──────────┘   └───────────────┘   └──────────┴───────────────┘
  配置 #1          配置 #2                 單次配置

一次配置使程式碼更小(只有一個配置呼叫)、執行更快(只配置一次),還可省去 control block 中部分簿記資訊。此分析同樣適用於 std::allocate_shared

單一 chunk 的代價:物件記憶體釋放會被 weak count 延後

control block 除了 reference count 外還有 weak count(追蹤 std::weak_ptr 數量)。只要還有 std::weak_ptr 指著 control block,該 chunk 就不能釋放。用 make_shared 建立的物件,即使最後一個 std::shared_ptr 已銷毀(物件解構子已呼叫),其記憶體要等到最後一個 std::weak_ptr 也銷毀才能釋放;直接用 new 則物件記憶體可在最後一個 shared_ptr 銷毀時立即釋放,之後只剩 control block 佔用。對超大型物件(如 ReallyBigType)+ 長壽 weak_ptr 的記憶體敏感系統,這是不用 make_shared 的正當理由。

make functions 的限制:何時不能/不該用

情況 影響範圍 原因
需要 custom deleter make_uniquemake_shared 都不行 make functions 沒有參數可指定 deleter;只能用 new + 建構子
想以 braced initializer 建構 兩者都不行 braced initializer 無法被完美轉發(見 Item 30)
類別自訂 operator new / operator delete make_shared / allocate_shared 不宜 類別專屬配置常只處理 sizeof(Widget) 大小,但 make_shared 要求「物件 + control block」的較大 chunk
超大物件 + weak_ptrshared_ptr 長壽 + 記憶體敏感 make_shared 不宜 見上方 warning:物件記憶體釋放被延後
// custom deleter:只能直接 new
auto widgetDeleter = [](Widget* pw) { /* ... */ };
std::unique_ptr<Widget, decltype(widgetDeleter)>
  upw(new Widget, widgetDeleter);
std::shared_ptr<Widget> spw(new Widget, widgetDeleter);
make functions 內部一律用小括號 () 轉發,不是大括號 {}

auto upv = std::make_unique<std::vector<int>>(10, 20); 建出的是 10 個元素、每個值 20 的 vector(呼叫非 initializer_list 建構子),而非 {10, 20} 兩個元素。想用 braced initializer 需借助 workaround:

auto initList = { 10, 20 };  // auto 推導出 std::initializer_list<int>
auto spv = std::make_shared<std::vector<int>>(initList);  // 2 個元素

必須直接用 new 時的安全寫法

若因 custom deleter 等原因不能用 make function,務必把 new 立即交給 smart pointer,且該語句不做其他事

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

// 錯誤示範:仍有洩漏風險(computePriority 可能插在中間拋例外)
processWidgetshared_ptr<Widget>(new Widget, cusDel,
              computePriority());                       // 潛在洩漏!

// 正確:獨立語句建構 smart pointer
std::shared_ptr<Widget> spw(new Widget, cusDel);
processWidgetmove(spw), computePriority();  // 例外安全且高效

Exam/Test Patterns

情境關鍵字 答案
std::make_unique 是哪個標準? C++14std::make_shared 是 C++11)
fshared_ptr<W>(new W), g() 為何可能洩漏? 編譯器可把 g() 排在 newshared_ptr 建構子之間g() 拋例外 → new 出的物件無人接管
make_sharednew 快在哪? 單次配置:物件與 control block 放同一 chunk(new 需兩次配置)
需要 custom deleter 時? 不能用 make functions,只能 new + smart pointer 建構子(獨立語句)
make_unique<vector<int>>(10, 20) 結果? 10 個元素、值皆 20(內部用小括號轉發,非 {10, 20}
想傳 braced initializer 給 make function? auto initList = { ... }; 建立 std::initializer_list,再傳入
類別自訂 operator new/delete 可以用 make_shared 嗎? 不宜:類別版配置只處理物件大小,無法容納物件 + control block
make_shared 建的大物件何時真正釋放記憶體? 最後一個 shared_ptr 最後一個 weak_ptr 都銷毀後
不能用 make 時的安全模式? new 結果立即在獨立語句交給 smart pointer,傳遞時用 std::move 避免 refcount 原子操作
自寫 C++11 版 make_unique 注意? 勿放入 namespace std,避免升級後與標準庫版本衝突