Item 18:以 std::unique_ptr 管理專屬所有權資源 (Use std::unique_ptr for Exclusive-Ownership Resource Management)
Overview Table
| 面向 | 重點 |
|---|---|
| 語意 | exclusive ownership(專屬所有權):非 null 的 std::unique_ptr 永遠擁有它指向的資源 |
| 大小/速度 | 預設(用 delete)與 raw pointer 同大小、同指令,適用於記憶體與時間都吃緊的場合 |
| 複製/移動 | move-only:不可複製;move 會轉移所有權,來源指標被設為 null |
| 銷毀方式 | 預設對內部 raw pointer 施加 delete;可在建構時指定 custom deleter |
| custom deleter 代價 | deleter 型別是 std::unique_ptr 型別的一部分;有狀態的 deleter 或 function pointer 會增加物件大小 |
| 典型用途 | 工廠函式回傳型別、Pimpl Idiom(見 Item 22) |
| 兩種形式 | std::unique_ptr<T>(單一物件,有 */-> 無 [])與 std::unique_ptr<T[]>(陣列,有 [] 無 */->) |
| 轉型 | 可輕鬆且高效地轉成 std::shared_ptr,故是工廠函式的最佳回傳型別 |
| 取代對象 | std::auto_ptr(C++98 遺物,已 deprecated,用複製冒充移動)——除非被迫用 C++98 編譯,一律改用 std::unique_ptr |
專屬所有權與 move-only 語意
- 非 null 的
std::unique_ptr一定擁有其指向的資源;解構時自動銷毀資源(預設delete)。 - 複製被禁止:若允許複製,會有兩個
std::unique_ptr都自認擁有(且該銷毀)同一資源 → double delete。 - 移動即轉移所有權:來源指標移動後被設為 null。
auto p1 = std::make_unique<Widget>(); // p1 擁有 Widget
// auto p2 = p1; // 錯誤!unique_ptr 不可複製
auto p2 = std::move(p1); // OK:所有權轉移給 p2,p1 變成 null
所有權遷移鏈(工廠 → 容器 → 資料成員 → 解構):
makeInvestment() std::move std::move
回傳 unique_ptr ───────► 容器元素 ───────► 物件的資料成員
│ │ │
└── 每一步來源都設為 null,永遠只有一個擁有者 ──┘
│
物件解構 ──► 成員 unique_ptr
解構 ──► 資源被銷毀
即使因例外、提早 return 或 break 中斷控制流,擁有資源的 std::unique_ptr 終究會呼叫解構子,資源必被釋放。
「解構子一定會被呼叫」的例外
少數異常終止情境下區域物件不會被解構:例外傳出執行緒的主函式(如 main)、違反 noexcept 規格(見 04-Moving-To-Modern-Cpp/08-Noexcept),或呼叫 std::abort / exit 類函式(std::_Exit、std::exit、std::quick_exit)時。
工廠函式回傳型別與 custom deleter
工廠函式在 heap 上建立階層物件並把銷毀責任交給呼叫者,與 std::unique_ptr 完美契合:
class Investment { public: virtual ~Investment(); /* … */ }; // 基底類別
class Stock: public Investment { /* … */ };
class Bond: public Investment { /* … */ };
class RealEstate: public Investment { /* … */ };
auto delInvmt = [](Investment* pInvestment) // custom deleter:
{ // 先記 log 再 delete
makeLogEntry(pInvestment);
delete pInvestment;
};
template<typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt)> // deleter 型別是
makeInvestment(Ts&&... params) // unique_ptr 型別的一部分
{
std::unique_ptr<Investment, decltype(delInvmt)>
pInv(nullptr, delInvmt); // 先建立 null 指標並綁定 deleter
if ( /* 應建立 Stock */ )
pInv.reset(new Stockforward<Ts>(params)...); // 完美轉發參數
else if ( /* 應建立 Bond */ )
pInv.reset(new Bondforward<Ts>(params)...);
else if ( /* 應建立 RealEstate */ )
pInv.reset(new RealEstateforward<Ts>(params)...);
return pInv; // 呼叫者用 auto 接收,完全不需知道刪除細節
}
實作要點:
| 要點 | 說明 |
|---|---|
| deleter 型別入型別 | custom deleter 的型別必須作為 std::unique_ptr 的第二個模板引數(用 decltype(delInvmt) 取得,見 02-Deducing-Types/03-Decltype) |
為何用 reset |
raw pointer 不能隱式轉換成 std::unique_ptr(直接指派 new 的結果不會編譯),需用 reset 讓 pInv 接管所有權 |
std::forward |
每次 new 都完美轉發呼叫者提供的參數(見 Item 25) |
| virtual destructor | deleter 以 Investment* 刪除衍生類別物件 → 基底類別必須有 virtual 解構子,否則是 undefined behavior |
| C++14 簡化 | 函式回傳型別推導讓 auto makeInvestment(Ts&&... params) 可把 deleter lambda 整個封裝進函式內 |
custom deleter 對大小的影響
「std::unique_ptr 與 raw pointer 同大小」只在預設 deleter(delete) 下成立:
| deleter 形式 | 對 std::unique_ptr 大小的影響 |
|---|---|
預設 delete |
與 raw pointer 相同(一個 word) |
| 無捕獲 lambda(stateless function object) | 零額外開銷——首選寫法 |
| function pointer | 從一個 word 變兩個 word |
| 有狀態的 function object(如有捕獲的 lambda) | 依儲存的狀態多寡而增大,可能顯著膨脹 |
Tip
deleter 能寫成函式也能寫成無捕獲 lambda 時,優先用 lambda:更高效(無大小懲罰)。若 custom deleter 讓 std::unique_ptr 大得無法接受,通常代表設計需要修改。
對比 std::shared_ptr
std::shared_ptr 的 deleter 不屬於其型別,也不改變其大小(固定兩個指標大小);這是兩者 API 設計的關鍵差異(見 05-Smart-Pointers/02-Shared-Ptr)。
陣列形式與轉換為 shared_ptr
- 兩種形式:
std::unique_ptr<T>(單一物件)與std::unique_ptr<T[]>(陣列),API 各自貼合用途——單一物件形式沒有operator[],陣列形式沒有operator*/operator->,指向的實體種類永無歧義。 - 陣列形式幾乎只有智識上的價值:
std::array、std::vector、std::string幾乎總是更好的選擇;唯一合理場景是接手 C-like API 回傳的 heap 陣列所有權。 - 輕鬆轉成
std::shared_ptr:
std::shared_ptr<Investment> sp = // std::unique_ptr 轉換成
makeInvestment( arguments ); // std::shared_ptr,簡單且高效
工廠函式無法預知呼叫者要專屬還是共享所有權;回傳 std::unique_ptr 提供最高效的智慧指標,又不妨礙呼叫者升級為更有彈性的 std::shared_ptr(反向轉換不存在)。
Exam/Test Patterns
| 情境關鍵字 | 答案 |
|---|---|
預設 std::unique_ptr 的大小與速度 |
與 raw pointer 同大小、同指令(僅限預設 deleter) |
複製 std::unique_ptr |
不能編譯——move-only 型別 |
move 後的來源 std::unique_ptr |
被設為 null |
把 new 的結果指派給 std::unique_ptr |
不能編譯(禁止 raw → smart 隱式轉換);用建構子或 reset |
| custom deleter 放哪裡 | std::unique_ptr 的第二個模板引數(型別的一部分) |
| function pointer 當 deleter | 大小從一個 word 增為兩個 word |
| 無捕獲 lambda 當 deleter | 無大小懲罰,優於函式指標 |
| deleter 以基底指標刪除衍生物件 | 基底類別需要 virtual destructor,否則 UB |
| 工廠函式該回傳哪種智慧指標 | std::unique_ptr(可再轉 std::shared_ptr,反向不行) |
unique_ptr<T[]> 何時合理 |
接手 C-like API 回傳的 heap 陣列;否則用 std::array/std::vector/std::string |
std::auto_ptr |
C++98 deprecated 遺物,複製冒充移動、不能放容器;一律改用 std::unique_ptr |
unique_ptr 常見兩大用途 |
工廠函式回傳型別、Pimpl Idiom |
Related Notes
- 05-Smart-Pointers/02-Shared-Ptr — 共享所有權;deleter 不屬於型別、大小固定兩個指標的對比設計
- 05-Smart-Pointers/03-Weak-Ptr — 可偵測懸空的
std::shared_ptr式指標 - 05-Smart-Pointers/04-Make-Unique-And-Make-Shared — 建立
std::unique_ptr的首選方式std::make_unique - 05-Smart-Pointers/05-Pimpl-Idiom —
std::unique_ptr最流行的應用場景之一 - 02-Deducing-Types/03-Decltype —
decltype(delInvmt)取得 deleter 型別、C++14 回傳型別推導 - 06-Move-Semantics-And-Perfect-Forwarding/03-Move-Vs-Forward-Usage — 工廠函式中以
std::forward完美轉發建構參數 - 04-Moving-To-Modern-Cpp/08-Noexcept — 違反 noexcept 導致區域物件不解構的例外情境
- 05-Smart-Pointers/Practice-Smart-Pointers — 本章練習題