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_ptr 比 std::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。
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_unique 與 make_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_ptr 比 shared_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);
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(); // 例外安全且高效
std::shared_ptr建構子即使自己拋出例外(如 control block 配置失敗),也保證對傳入的裸指標呼叫 deleter,故獨立語句版本安全。- 傳
spw(lvalue)會複製shared_ptr→ 原子遞增 reference count;套上std::move變 rvalue 則只需 move、完全不動 reference count,效能與原本傳 rvalue 的寫法相同。
Exam/Test Patterns
| 情境關鍵字 | 答案 |
|---|---|
std::make_unique 是哪個標準? |
C++14(std::make_shared 是 C++11) |
fshared_ptr<W>(new W), g() 為何可能洩漏? |
編譯器可把 g() 排在 new 與 shared_ptr 建構子之間,g() 拋例外 → new 出的物件無人接管 |
make_shared 比 new 快在哪? |
單次配置:物件與 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,避免升級後與標準庫版本衝突 |
Related Notes
- 05-Smart-Pointers/01-Unique-Ptr —
std::unique_ptr基礎與 custom deleter 對型別的影響(Item 18) - 05-Smart-Pointers/02-Shared-Ptr — control block 的組成與產生規則,是理解單次配置優勢的前提(Item 19)
- 05-Smart-Pointers/03-Weak-Ptr — weak count 為何延後
make_shared記憶體釋放(Item 20) - 04-Moving-To-Modern-Cpp/01-Braced-Initialization —
()vs{}與std::initializer_list建構子優先權(Item 7) - 06-Move-Semantics-And-Perfect-Forwarding/08-Perfect-Forwarding-Failure-Cases — braced initializer 無法完美轉發的原理與 workaround(Item 30)
- 06-Move-Semantics-And-Perfect-Forwarding/01-Std-Move-And-Std-Forward —
std::move(spw)讓例外安全寫法不損失效能(Item 23) - 09-Tweaks/02-Emplacement — emplacement 同樣涉及完美轉發與
()vs{}的抉擇(Item 42)