Item 19:以 std::shared_ptr 管理共享所有權資源 (Use std::shared_ptr for Shared-Ownership Resource Management)

Overview Table

面向 重點
語意 共享所有權(shared ownership):沒有任何單一 std::shared_ptr 獨占物件,最後一個停止指向該物件的 std::shared_ptr 負責銷毀它
賣點 接近 garbage collection 的便利性 + destructor 的確定性時機(deterministic timing),且適用於任意資源(不限記憶體)
大小 通常是 raw pointer 的兩倍(指向物件的指標 + 指向 control block 的指標)
執行期成本 control block 需動態配置;reference count 的增減必須是 atomic 操作
Deleter 預設用 delete;支援 custom deleter,且 deleter 型別不屬於 std::shared_ptr 型別的一部分(與 std::unique_ptr 相反)
頭號地雷 避免用 raw pointer 變數建構 std::shared_ptr——會產生多個 control block,導致重複銷毀(undefined behavior)
限制 單向道:轉成 std::shared_ptr 後無法變回 std::unique_ptr;不支援陣列(無 std::shared_ptr<T[]>

共享所有權與 Reference Count

物件的 reference count 記錄目前有多少 std::shared_ptr 指向它:建構子(通常)遞增、解構子遞減、copy assignment 兩者皆做。遞減後見到 0 的那個 std::shared_ptr 負責銷毀物件。

auto spw1 = std::make_shared<Widget>();  // RC = 1
auto spw2 = spw1;                        // copy:RC = 2(atomic 遞增)
spw1 = nullptr;                          // RC = 1
spw2 = nullptr;                          // RC = 0 → Widget 被銷毀(時機確定)

Reference count 帶來三項效能代價:

代價 說明
兩倍大小 內含兩個 raw pointer:指向資源 + 指向 reference count(control block)
動態配置 被指物件不知道自己被計數,count 必須另行配置在 heap 上(好處:任何型別、甚至內建型別都能被管理);用 std::make_shared 可省下這次獨立配置
Atomic 操作 不同執行緒可能同時讀寫同一 count(一邊解構、一邊複製),故增減必須 atomic——通常仍是單一機器指令,但比非 atomic 慢
「建構子遞增 reference count」的例外:move

Move construction / move assignment 不動 reference count——來源 std::shared_ptr 被設為 null,「舊的停、新的起」同時發生,無須計數操作。因此 move 比 copy 快(建構與賦值皆然)。

「兩倍大小」並非標準強制

Standard 並未要求此實作方式,但作者所知的每一個標準函式庫實作皆如此。另注意:atomic 保護的只有 reference count 本身,被指物件的存取並無任何執行緒同步

Control Block:結構與建立規則

Reference count 其實只是 control block 的一部分。每個由 std::shared_ptr 管理的物件各有一個 control block,除 reference count 外還存放 weak count、custom deleter / allocator 副本等:

   std::shared_ptr<T>
  +--------------------+        +-------------+
  |      Ptr to T      | -----> |  T Object   |
  +--------------------+        +-------------+
  | Ptr to Control Blk | --+
  +--------------------+   |    +----------------------------+
                           +--> |       Control Block        |
                                |  Reference Count           |
                                |  Weak Count                |
                                |  Other Data                |
                                |  (custom deleter/allocator |
                                |   等,若有指定)            |
                                +----------------------------+

Control block 由「建立第一個指向該物件的 std::shared_ptr」的函式建立,規則如下:

建構方式 是否建立新 control block
std::make_shared(見 Item 21) 一定建立(物件是全新製造的,不可能已有 control block)
std::unique_ptr(或 std::auto_ptr)建構 建立(unique-ownership 指標不用 control block;來源指標被設為 null)
傳入 raw pointer 建立(假設該物件還沒有 control block)
傳入 std::shared_ptrstd::weak_ptr 不建立,沿用既有 control block
同一 raw pointer 建構多個 std::shared_ptr = undefined behavior

多個 control block ⇒ 多個 reference count ⇒ 物件被銷毀多次

auto pw = new Widget;                          // raw pointer 變數(壞味道)
std::shared_ptr<Widget> spw1(pw, loggingDel);  // 為 *pw 建立 control block
std::shared_ptr<Widget> spw2(pw, loggingDel);  // 為 *pw 建立第二個 control block!

兩個教訓:(1) 盡量別把 raw pointer 傳給 std::shared_ptr 建構子,優先用 std::make_shared(但需要 custom deleter 時不能用它);(2) 非傳不可時,直接傳 new 的結果,不要經過 raw pointer 變數:
std::shared_ptr<Widget> spw1(new Widget, loggingDel); 之後第二個指標應寫 std::shared_ptr<Widget> spw2(spw1);(copy 建構,共用 control block,完全沒問題)。

成本總結:典型情況(預設 deleter + 預設 allocator + std::make_shared 建立)下 control block 只有約三個 word,配置幾乎免費(併入物件本身的配置);dereference 與 raw pointer 一樣快;control block 內部用到繼承與 virtual function(確保物件被正確銷毀),但該機制通常每個物件只用一次——銷毀時。整體而言,std::shared_ptr相當合理的成本換得自動生命週期管理。

Warning

Custom deleter 與 custom allocator 會讓 control block 變大;deleter 若是含大量狀態的 function object,這些資料存放在 control block(heap,或 allocator 管理的記憶體)而非 std::shared_ptr 物件本身。

Custom Deleter:與 std::unique_ptr 的關鍵差異

比較 std::unique_ptr std::shared_ptr
Deleter 型別 是智慧指標型別的一部分(第二個 template 參數) 不是型別的一部分
指定 deleter 後的物件大小 可能變大(function pointer / 有狀態 functor) 不變,永遠兩個指標大(deleter 存於 control block)
不同 deleter 的指標能否互相賦值、放同一容器、傳同一函式參數 不能(型別不同) 可以(型別相同)
陣列支援 std::unique_ptr<T[]> std::shared_ptr<T[]>(見下方 warning)
auto loggingDel = [](Widget* pw) {        // custom deleter
  makeLogEntry(pw);
  delete pw;
};

std::unique_ptr<Widget, decltype(loggingDel)>  // deleter 型別是
  upw(new Widget, loggingDel);                 // 指標型別的一部分

std::shared_ptr<Widget>                        // deleter 型別不是
  spw(new Widget, loggingDel);                 // 指標型別的一部分

// 兩個 deleter 型別不同的 shared_ptr 仍是同型別,可放進同一容器:
auto customDeleter1 = [](Widget* pw) { /* ... */ };
auto customDeleter2 = [](Widget* pw) { /* ... */ };
std::shared_ptr<Widget> pw1(new Widget, customDeleter1);
std::shared_ptr<Widget> pw2(new Widget, customDeleter2);
std::vector<std::shared_ptr<Widget>> vpw{ pw1, pw2 };  // OK
「shared_ptr 不能管陣列」是 C++11/14 的規則

原書時代沒有 std::shared_ptr<T[]>,用 custom deleter 做 delete[] 雖可編譯,卻是餿主意(無 operator[]、derived-to-base 轉換對陣列會打穿型別系統);優先考慮 std::arraystd::vectorstd::stringC++17 起標準已加入 std::shared_ptr<T[]>,但「別用智慧指標指向笨陣列」的設計忠告依然成立。

this 指標陷阱與 std::enable_shared_from_this

this 傳給 std::shared_ptr(或裝著它的容器)= 傳 raw pointer ⇒ 建立新的 control block。若外部已有 std::shared_ptr 指向此物件,即為 undefined behavior:

std::vector<std::shared_ptr<Widget>> processedWidgets;

void Widget::process() {
  processedWidgets.emplace_back(this);   // 錯!this 是 raw pointer,
}                                        // 會另建 control block

解法:繼承 std::enable_shared_from_this<Widget>CRTP,The Curiously Recurring Template Pattern:base class 以 derived class 為 template 參數),並改呼叫 shared_from_this()——它查出現有的 control block 並據以建立新的 std::shared_ptr,不會重複建立:

class Widget : public std::enable_shared_from_this<Widget> {
public:
  template<typename... Ts>                               // factory:完美轉發
  static std::shared_ptr<Widget> create(Ts&&... params); // 給 private 建構子

  void process();
private:
  Widget();                       // 建構子設為 private,
};                                // 強迫客戶經由 create 取得 shared_ptr

void Widget::process() {
  processedWidgets.emplace_back(shared_from_this());  // 安全:共用既有
}                                                     // control block
shared_from_this 的前提

呼叫 shared_from_this() 之前,必須已存在指向該物件的 std::shared_ptr(即物件已有 control block),否則行為未定義(實作上通常丟出例外)。這正是把建構子設 private、只透過回傳 std::shared_ptr 的 factory 函式建立物件的原因。

所有權是單向道

std::unique_ptrstd::shared_ptr 的「升級」容易;反向不可能——即使 reference count 為 1,也無法把資源所有權從 std::shared_ptr 收回交給 std::unique_ptr。若獨占所有權就夠用(或可能夠用),直接選 std::unique_ptr(見 Item 18),其效能貼近 raw pointer。

Exam/Test Patterns

情境關鍵字 答案 shared_ptr 的大小 通常是 raw pointer 的 2 倍(object ptr + control block ptr);custom deleter 不會改變其大小 reference count 為何要 atomic 多執行緒可能同時增減同一 count;atomic 較慢但通常仍是單一指令 move 一個 shared_ptr 會發生什麼 來源設為 null、不動 reference count,所以 move 比 copy 快 哪些建構方式會建立新 control block make_shared、從 unique-ownership 指標建構、傳入 raw pointer;傳入 shared_ptr/weak_ptr 則否 同一 raw pointer 建兩個 shared_ptr 兩個 control block、兩個 count ⇒ 重複 delete ⇒ undefined behavior 成員函式想把 this 交給 shared_ptr 繼承 std::enable_shared_from_this(CRTP),呼叫 shared_from_this();前提是物件已被某個 shared_ptr 管理 deleter 型別不同的兩個 shared_ptr 型別相同,可互相賦值、放進同一容器(unique_ptr 做不到) shared_ptr 轉回 unique_ptr 不可能,所有權移交是單向且永久的 shared_ptr 管理陣列 C++11/14 無 shared_ptr<T[]>;應改用 std::array / std::vector / std::string 需要 custom deleter 時能否用 make_shared 不能;必須直接傳 new 的結果給建構子(勿經 raw pointer 變數)