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) |
| 三大使用場景 | caching、observer 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(懸空可偵測)
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;
}
此為 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 被銷毀 |
用 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 操作 |
「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 才能偵測懸空 |
Related Notes
- 05-Smart-Pointers/02-Shared-Ptr——
std::weak_ptr是std::shared_ptr的擴充,共用其 control block 與 reference count 機制 - 05-Smart-Pointers/01-Unique-Ptr——factory 預設回傳
std::unique_ptr;階層式結構的 parent→child 連結也應用它 - 05-Smart-Pointers/04-Make-Unique-And-Make-Shared——control block 中第二個 reference count(weak count)的細節,以及
make_shared與 weak count 對記憶體釋放時機的影響 - 01-Introduction/01-Terms-And-Conventions——dangling pointer 與 undefined behavior 的術語定義
- 08-Concurrency-API/06-Atomic-Vs-Volatile——reference count 操作為 atomic 的背景知識