Item 20:對可能懸空的 std::shared_ptr-like 指標使用 std::weak_ptr (Use std::weak_ptr for std::shared_ptr-like Pointers That Can Dangle)

Overview Table

問題 答案
std::weak_ptr 是什麼 std::shared_ptr擴充(augmentation),不是獨立的 smart pointer;指向同一物件但不參與 shared ownership
影響 reference count 嗎 不影響被指物件的 shared count(但會操作 control block 的 weak count
能 dereference 或測 null 嗎 都不能——API 刻意受限,必須先轉回 std::shared_ptr
懸空(dangle)時稱為 expired(過期),可用 wpw.expired() 檢查
安全存取方式 wpw.lock()(expired 得 null shared_ptr)或 std::shared_ptr 建構子(expired 擲出 std::bad_weak_ptr
三大使用場景 cachingobserver lists打破 std::shared_ptr cycles
成本 std::shared_ptr 同大小、共用同一 control block、操作涉及 atomic 的 weak count 增減

weak_ptr 的本質:會偵測自己懸空的非擁有指標

std::weak_ptr 解決一個 std::shared_ptr 不會遇到的問題:指向的物件可能已被銷毀。它像 std::shared_ptr 一樣指向物件,卻不影響物件的 reference count,並能追蹤自己是否懸空

auto spw = std::make_shared<Widget>();  // 建構後 Widget 的
                                        // ref count (RC) 為 1

std::weak_ptr<Widget> wpw(spw);         // wpw 指向同一個 Widget,
                                        // RC 仍為 1(weak_ptr 不增加 RC)

spw = nullptr;                          // RC 歸 0,Widget 被銷毀,
                                        // wpw 現在懸空(dangle)

if (wpw.expired()) { /* ... */ }        // 懸空的 weak_ptr 稱為 expired,
                                        // 可直接檢測
  spw 建構            wpw(spw) 建立           spw = nullptr
     |                     |                       |
     v                     v                       v
 RC = 1  ────────────► RC = 1(不變)────────► RC = 0,Widget 銷毀
                       wpw 指向 Widget          wpw → expired(懸空可偵測)
Important

std::weak_ptr 不能被 dereference、也不能測試是否為 null。它是 std::shared_ptr 的附屬品:通常由 std::shared_ptr 建立,也只能透過轉回 std::shared_ptr 來存取物件。

安全存取:lock() 與 shared_ptr 建構子

先呼叫 expired() 再自行 dereference 是不可行的——即使語法上做得到,檢查與存取分離會引入 race condition:兩步驟之間,另一條執行緒可能銷毀最後一個 std::shared_ptr,導致 dereference 產生 undefined behavior。因此需要一個原子(atomic)操作:檢查是否 expired,若否則同時取得存取權。做法是std::weak_ptr 建立 std::shared_ptr,有兩種形式:

形式 expired 時的行為 適用情境
wpw.lock() 回傳 null 的 std::shared_ptr 允許「物件可能已消失」的正常流程(如 cache 查詢)
std::shared_ptr<Widget> spw(wpw) 擲出 std::bad_weak_ptr 例外 物件不存在即屬錯誤的情境
std::shared_ptr<Widget> spw1 = wpw.lock();  // 若 wpw 已 expired,
                                            // spw1 為 null

auto spw2 = wpw.lock();                     // 同上,改用 auto

std::shared_ptr<Widget> spw3(wpw);          // 若 wpw 已 expired,
                                            // 擲出 std::bad_weak_ptr

三大使用場景:caching、observer、打破 cycle

場景 為何用 std::weak_ptr
Caching factory cache 不該控制物件生命週期,但必須能偵測 cache 項目是否已懸空
Observer pattern subject 不控制 observer 的生命週期,但要確保不存取已銷毀的 observer
打破 shared_ptr cycle 互指的 std::shared_ptr 形成循環會造成洩漏;weak_ptr 不增加 RC,可安全回指

場景一:快取版 factory。昂貴的 loadWidget(回傳 std::unique_ptr,見 Item 18)改為快取版時,回傳型別必須改成 std::shared_ptr——因為只有物件生命週期由 std::shared_ptr 管理時,std::weak_ptr 才能偵測懸空

std::shared_ptr<const Widget> fastLoadWidget(WidgetID id)
{
  static std::unordered_map<WidgetID,
                            std::weak_ptr<const Widget>> cache;

  auto objPtr = cache[id].lock();   // objPtr 是指向快取物件的
                                    // shared_ptr(不在快取則為 null)
  if (!objPtr) {                    // 不在快取:
    objPtr = loadWidget(id);        // 載入
    cache[id] = objPtr;             // 存入快取
  }
  return objPtr;
}
Warning

此為 quick-and-dirty 實作:cache 會累積已 expired 的 std::weak_ptr(對應已銷毀的 Widget),實務上需定期清除。

**場景三:回指造成的 cycle。**A、C 共享 B 的所有權,若 B 需要回指 A:

        std::shared_ptr        std::shared_ptr
   A ───────────────────► B ◄─────────────────── C
   ▲                      │
   └──────── ??? ─────────┘   B 回指 A 該用什麼指標?
選項 問題
raw pointer A 被銷毀後 B 無法偵測懸空,dereference 產生 undefined behavior
std::shared_ptr A、B 互指形成 cycle,RC 永不歸 0——即使無法從程式其他處存取,兩者也永遠洩漏
std::weak_ptr 最佳解:A 銷毀後 B 可偵測懸空;且不影響 A 的 RC,不會阻止 A 被銷毀
Warning

std::weak_ptr 打破 cycle 的需求其實不常見:在嚴格階層式結構(如樹)中,parent→child 用 std::unique_ptr 即可,child→parent 用 raw pointer 就安全——因為 child 的生命週期絕不會長於 parent。std::weak_ptr 主要用於非階層式資料結構、caching 與 observer lists。

效能真相:同大小、同 control block、第二個 reference count

std::weak_ptr 的效率特性與 std::shared_ptr 基本相同

項目 內容
物件大小 std::shared_ptr 相同(兩個指標)
control block 共用 std::shared_ptr 的 control block(見 Item 19)
建構 / 解構 / 指派 涉及 atomic 的 reference count 操作
Warning

weak_ptr 不參與 reference counting」是簡化說法。精確地說:它不參與 shared ownership,因此不影響被指物件的 reference count;但 control block 中還有第二個 reference count(weak count)std::weak_ptr 操作的正是它(細節見 Item 21)。

Exam/Test Patterns

情境關鍵字 答案
「像 shared_ptr 但不影響 ref count、可能懸空」 std::weak_ptr(會追蹤自己是否 dangle)
「weak_ptr 可以 dereference / 測 null 嗎」 不行;它不是獨立 smart pointer,是 shared_ptr 的 augmentation
「先 expired() 再取值有什麼問題」 race condition:兩步之間物件可能被其他執行緒銷毀 → UB;必須用原子操作
「lock() 在 expired 時回傳什麼」 null 的 std::shared_ptr
「用 weak_ptr 建構 shared_ptr、但已 expired」 擲出 std::bad_weak_ptr
「weak_ptr 的典型使用場景」 caching、observer lists、打破 shared_ptr cycles
「A↔B 互指 shared_ptr 會怎樣」 cycle → RC 永不歸 0 → 資源洩漏;回指改用 weak_ptr
「樹狀結構 child 回指 parent 用什麼」 raw pointer 即可(child 生命週期不會長於 parent)
「weak_ptr 的大小與成本」 shared_ptr 同大小、共用 control block、操作 weak count(atomic)
「cache 版 factory 為何回傳 shared_ptr 而非 unique_ptr」 只有 shared_ptr 管理生命週期時,weak_ptr 才能偵測懸空