C++ Learning

第39回 std::shared_ptr と参照カウント ★目玉

複数の所有者で資源を共有するスマートポインタ。参照カウントを裏で管理し、所有者がゼロになったとき初めて資源を解放します。本回はカウンタが増減する様子をリアルタイム可視化で目に焼き付け、循環参照の罠と make_shared の最適化まで一気に整理します。

このページで押さえること
✅ 最低限ここだけ覚える
  • auto p = std::make_shared<T>(args...); で作る
  • コピーで参照カウント +1、破棄で -1
  • カウントが 0 になったとき初めて資源解放
  • 使う頻度は unique_ptr より少ない(まず unique、共有が必要なら shared)
⭐ 余裕があれば読む
  • 制御ブロック(refcount + weakcount + deleter)
  • make_shared と new の違い
  • 循環参照 → weak_ptr で解決
  • スレッド安全性の範囲

1. まず触ってみる ― 参照カウントの可視化

shared_ptr は「資源の共有」を参照カウントで管理します。コピーするとカウントが増え、破棄されると減り、ゼロになった時点で資源が解放される ― この動きをインタラクティブに見てみましょう。

▶ shared_ptr の参照カウントを動かす
a
(未作成)
b
(未作成)
c
(未作成)

最小コード

first_shared.cpp最小例
#include <memory> auto a = std::make_shared<int>(42); // use_count = 1 { auto b = a; // use_count = 2 (コピー) std::cout << a.use_count(); // 2 } // b が破棄 → use_count = 1 // スコープ終了 → use_count = 0 → delete
ここまでで覚えること(3 つ):
  • make_shared で作る(#include <memory>
  • コピーで +1、破棄で -1、0 で解放
  • 現在の所有者数は p.use_count() で確認できる

よくある素朴な疑問

Q. カウンタはどこに保存される?
→ shared_ptr オブジェクト自体ではなく、別のヒープ領域(制御ブロック)に保存されます。次の §2 で詳しく。

Q. unique_ptr とどっちを使えばいい?
→ 原則まず unique_ptr を検討、共有所有が本当に必要なときだけ shared_ptr。参照カウントの管理にわずかなオーバーヘッドがあるため。

Q. スレッドセーフ?
参照カウントの増減はスレッドセーフ(アトミック操作)。ただし中身のオブジェクト自体はスレッドセーフではないので、共有するならロック等で保護が必要。

2. 内部構造 ― 制御ブロック

shared_ptr の賢さは「制御ブロック (control block)」にあります。管理対象のポインタカウンタを分離して持ちます。

shared_ptr の構造内部
// shared_ptr は概念的に… class shared_ptr<T> { T* ptr_; // 管理対象 ControlBlock* cb_; // 共有される制御ブロック }; struct ControlBlock { atomic<long> use_count; // shared_ptr の数 atomic<long> weak_count; // weak_ptr の数(§4 で) Deleter deleter; // 解放関数(通常は delete) };

制御ブロックはコピーした全 shared_ptr で共有されます。そのため a = b したとき、カウンタが正しく増減する仕組み。

イメージ図

shared_ptr a, b, c が同じオブジェクトを共有する場合:
a ─┐
b ─┼──→ [制御ブロック: count=3] ──→ [オブジェクト実体]
c ─┘

3. make_shared vs new

shared_ptr の作成には 2 通りあります。

new 経由古い
std::shared_ptr<T> p(new T(42)); // ヒープ確保が 2 回: // ① T のオブジェクト // ② 制御ブロック
make_shared推奨
auto p = std::make_shared<T>(42); // ヒープ確保が 1 回: // [制御ブロック + T] を同じ領域に
make_shared の利点:
  • ヒープ確保が 1 回で済む(キャッシュ効率も良い)
  • 例外安全性が高い
  • 書き方がシンプル
make_shared の注意点: 制御ブロックとオブジェクトが同じメモリブロックにあるので、weak_ptr が生きている間はオブジェクトのメモリも解放されない(参照カウントが 0 でも)。巨大オブジェクトに多数の weak_ptr がある場合は new 経由のほうがメモリを早く解放できることも。ただし稀なケース。

4. 循環参照という罠

shared_ptr 最大の落とし穴。2 つ以上のオブジェクトが相互に shared_ptr を持つと、参照カウントが 0 にならずメモリリーク。

循環参照の例メモリリーク
struct Node { std::shared_ptr<Node> next; }; auto a = std::make_shared<Node>(); auto b = std::make_shared<Node>(); a->next = b; // b の count = 2 b->next = a; // a の count = 2 (循環!) // スコープ終了: // a が破棄 → count 2→1、b が破棄 → count 2→1 // 両方とも count = 1 のまま → 解放されない!
🔁 循環参照:両方とも count = 1 のまま(本来 0 になって解放されるべき)
Node a
next → b
use_count = 1
Node b
next → a
use_count = 1
スコープを抜けても count ≠ 0 なのでデストラクタが呼ばれずリーク

解決策は片方を std::weak_ptr にすること(次回 §35 で詳解)。所有権のあるほうが shared_ptr、参照だけのほうが weak_ptr、と設計します。

weak_ptr で解決推奨
struct Node { std::shared_ptr<Node> next; std::weak_ptr<Node> prev; // ← 所有しない }; // weak_ptr は参照カウントを増やさないので循環が切れる
典型パターン:「親 → 子」は shared_ptr(親が子を所有)、「子 → 親」は weak_ptr(子は親を参照するだけ)。木構造やグラフでよく使います。
ここまでで shared_ptr の基本は OK
最後は「いつ使うか」の判断基準。実務では使い過ぎ注意。

5. いつ shared_ptr を使うか

使うべきケース

使わなくていいケース

shared_ptr は使い過ぎない: 「所有権がはっきりしないから shared_ptr にしておこう」は設計の逃げ。所有関係を明確化する努力を先に。ちゃんと設計すると、ほとんどの場合 unique_ptr で足りることが分かります。
判断フロー:
  1. 所有者は 1 人? → unique_ptr
  2. 複数の所有者が共有? → shared_ptr
  3. 所有せず見るだけ? → 参照/生ポインタ/weak_ptr
広告スペース

確認クイズ

shared_ptr を 4 問で確認。

Q1. auto a = make_shared<int>(42); auto b = a; auto c = b; の後、a.use_count() は?

1
2
3
不定
a / b / c の 3 つが同じオブジェクトを共有しているので use_count は 3。どの shared_ptr からも同じカウンタを参照します。

Q2. make_sharedshared_ptr<T>(new T()) より優れている主な点は?

参照カウントが 0 から始まる
コピーが自動で行われる
ヒープ確保が 1 回で済む(通常 2 回が 1 回に)
スレッドセーフになる
make_shared はオブジェクトと制御ブロックを同じメモリブロックに確保。ヒープ確保回数が半分、キャッシュ効率も良い、例外安全性も向上。

Q3. 2 つの shared_ptr が相互に指し合う「循環参照」の結果は?

コンパイルエラー
実行時エラー
参照カウントが 0 にならず、メモリリーク
自動で片側が weak_ptr に変換される
参照カウントはローカル変数の破棄で減りますが、相互参照があるとカウントがゼロにならないためデストラクタが呼ばれずメモリリーク。解決策は片側を weak_ptr に。

Q4. 次のケースで最も適切な型は?
「ある関数に、既存のオブジェクトを所有せず参照だけ渡したい」

std::shared_ptr<T>
const T& (または T*)
std::unique_ptr<T>
std::weak_ptr<T>
「所有せず見るだけ」なら参照か生ポインタ。shared_ptr で渡すと参照カウントがインクリメント/デクリメントされて無駄、さらに「所有権を渡す」という意図にも読めてしまう。