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 語意

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::_Exitstd::exitstd::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 的結果不會編譯),需用 resetpInv 接管所有權
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::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