Select a result to preview
| 面向 | 重點 |
|---|---|
| 語意 | 共享所有權(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 記錄目前有多少 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 慢 |
Move construction / move assignment 不動 reference count——來源 std::shared_ptr 被設為 null,「舊的停、新的起」同時發生,無須計數操作。因此 move 比 copy 快(建構與賦值皆然)。
Standard 並未要求此實作方式,但作者所知的每一個標準函式庫實作皆如此。另注意:atomic 保護的只有 reference count 本身,被指物件的存取並無任何執行緒同步。
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_ptr 或 std::weak_ptr |
不建立,沿用既有 control block |
多個 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 以相當合理的成本換得自動生命週期管理。
Custom deleter 與 custom allocator 會讓 control block 變大;deleter 若是含大量狀態的 function object,這些資料存放在 control block(heap,或 allocator 管理的記憶體)而非 std::shared_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
原書時代沒有 std::shared_ptr<T[]>,用 custom deleter 做 delete[] 雖可編譯,卻是餿主意(無 operator[]、derived-to-base 轉換對陣列會打穿型別系統);優先考慮 std::array、std::vector、std::string。C++17 起標準已加入 std::shared_ptr<T[]>,但「別用智慧指標指向笨陣列」的設計忠告依然成立。
把 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() 之前,必須已存在指向該物件的 std::shared_ptr(即物件已有 control block),否則行為未定義(實作上通常丟出例外)。這正是把建構子設 private、只透過回傳 std::shared_ptr 的 factory 函式建立物件的原因。
std::unique_ptr → std::shared_ptr 的「升級」容易;反向不可能——即使 reference count 為 1,也無法把資源所有權從 std::shared_ptr 收回交給 std::unique_ptr。若獨占所有權就夠用(或可能夠用),直接選 std::unique_ptr(見 Item 18),其效能貼近 raw pointer。
make_shared、從 unique-ownership 指標建構、傳入 raw pointer;傳入 shared_ptr/weak_ptr 則否std::enable_shared_from_this(CRTP),呼叫 shared_from_this();前提是物件已被某個 shared_ptr 管理shared_ptr<T[]>;應改用 std::array / std::vector / std::stringnew 的結果給建構子(勿經 raw pointer 變數)emplace_back(this) 範例中 emplacement 的行為