C++ Learning

第40回 std::weak_ptr と循環参照

shared_ptr を「所有せず」参照するだけのスマートポインタ。参照カウントを増やさないので循環参照を断ち切れる。「まだ生きてる?」を確認したいオブザーバ・キャッシュ等で活躍。使うときは lock() で一時的に shared_ptr へ昇格します。

このページで押さえること
✅ 最低限ここだけ覚える
  • weak_ptr は shared_ptr の所有しない参照
  • 作り方:std::weak_ptr<T> w = sp;
  • 使うときは w.lock() で shared_ptr を得る
  • オブジェクトが消えていたら expired() が true
⭐ 余裕があれば読む
  • 制御ブロックの weak count
  • オブザーバパターンでの典型使用
  • 親子構造:親=shared, 子→親=weak
  • キャッシュでの使い方

1. まず触ってみる ― shared が死ぬと weak はどうなる?

weak_ptr は shared_ptr への「弱い参照」。元の shared_ptr がすべて破棄されると、weak_ptr は自動的に「期限切れ」の状態になります。

▶ weak_ptr の挙動
shared_ptr sp
(未作成)
weak_ptr wp
(未作成)
lock() の結果
(未取得)

最小コード

first_weak.cpp最小例
#include <memory> std::weak_ptr<int> wp; { auto sp = std::make_shared<int>(42); wp = sp; // weak_ptr に代入(カウントは増えない) if (auto tmp = wp.lock()) { // 一時的に shared_ptr に昇格 std::cout << *tmp; // 42 } } // sp 破棄 → ヒープも解放 if (wp.expired()) { std::cout << "もう死んでる"; } if (auto tmp = wp.lock()) { // nullptr が返る // このブロックには入らない }
ここまでで覚えること(3 つ):
  • weak_ptr は shared_ptr から作る(参照カウントは増えない)
  • 使うときは lock() で一時的な shared_ptr を得る
  • 元が死んでいたら lock() は空の shared_ptr を返す

よくある素朴な疑問

Q. なぜ直接参照じゃなく weak_ptr?
→ 「元のオブジェクトが生きているか」を安全にチェックできるから。生ポインタでは解放されたアドレスにアクセスしてしまう(dangling)。weak_ptr なら lock() で安全確認付きでアクセスできます。

Q. w->method() のように直接使えない?
できません。 weak_ptr にはデリファレンス演算子がない。必ず lock() して shared_ptr を得てから使います。これが「確認してから使う」の強制。

2. lock() の使い方

weak_ptr から中身を取り出す方法は 2 つ:

lock()推奨
auto sp = wp.lock(); if (sp) { // まだ生きている use(*sp); } else { // 期限切れ }
expired()先にチェック
if (!wp.expired()) { auto sp = wp.lock(); use(*sp); } // ※ マルチスレッドだと expired→lock の間に消える可能性あり // lock() 一発の方が安全
推奨: if (auto sp = wp.lock()) の形が安全で簡潔。これなら「取れたら使う」がアトミックに書けます。

3. 循環参照を解決する

前章(shared_ptr)で触れた循環参照。これを weak_ptr で断ち切ります。

循環参照(リーク)NG
struct Node { std::shared_ptr<Node> next; std::shared_ptr<Node> prev; // ← 両方 shared で循環 }; auto a = std::make_shared<Node>(); auto b = std::make_shared<Node>(); a->next = b; b->prev = a; // スコープ終了後も count が 1 で残る → リーク
weak_ptr で解決OK
struct Node { std::shared_ptr<Node> next; std::weak_ptr<Node> prev; // ← 所有しない }; // 所有:next (forward 方向) // 参照:prev (back 方向、弱い) // → スコープ終了で正しく解放

判断基準

ツリー構造の定番:
  • Tree node の children は std::vector<std::shared_ptr<Node>>
  • parent 参照は std::weak_ptr<Node>
これで double-linked な構造でもリークしません。
ここまでで日常は OK
最後は用途別のパターン。オブザーバやキャッシュで効いてきます。

4. 典型用途パターン

① オブザーバパターン

観測者を weak で持つ定番
class Subject { std::vector<std::weak_ptr<Observer>> obs_; public: void notify() { for (auto& w : obs_) { if (auto o = w.lock()) { // 生きていれば通知 o->on_event(); } // 死んでいればスキップ(自動で掃除も可) } } };

Subject が Observer を weak_ptr で持てば、Observer の寿命を Subject が縛らない。Observer 側の都合で先に消えても、Subject は安全にスキップできます。

② キャッシュ

使われていないキャッシュは消える省メモリ
std::map<std::string, std::weak_ptr<Texture>> cache; std::shared_ptr<Texture> get(const std::string& key) { if (auto sp = cache[key].lock()) return sp; // ヒット auto sp = load(key); // ミス → 読み込み cache[key] = sp; // weak で保持 return sp; }

使われている間は shared_ptr で保持され続け、使用箇所がゼロになったら自動で消えるキャッシュ。

③ shared_from_this

自分自身の shared_ptr を作る継承
class Foo : public std::enable_shared_from_this<Foo> { public: void register_self() { // this から shared_ptr が欲しい場面 registry.add(shared_from_this()); } }; // ※ Foo は必ず make_shared で作られている必要がある

メンバ関数の中で「自分を shared_ptr として保存したい」ケース(コールバック登録など)に使う。内部的に weak_ptr の仕組みを利用。

shared_from_this の制約:
  • このクラスのインスタンスは必ず make_shared / shared_ptr で作られている必要
  • コンストラクタ・デストラクタ内では呼べない(まだ shared_ptr が確定していない / もう切れている)
広告スペース

確認クイズ

weak_ptr を 4 問で確認。

Q1. weak_ptr が shared_ptr と違う最大のポイントは?

スレッドセーフでない
中身のコピーを持つ
参照カウントを増やさない(所有しない)
null になれない
weak_ptr は制御ブロックの weak_count は増やしますが、use_count は増やしません。つまりオブジェクトの所有権を持ちません。

Q2. weak_ptr から中身を使う正しい方法は?

*wp でそのまま使う
wp.lock() で shared_ptr を得てから使う
wp.get() で生ポインタを取る
static_cast で shared_ptr に変換
weak_ptr はそのままではアクセスできません。lock() で一時的な shared_ptr を作り、それが空でなければ使用。これによって「使っている間は絶対に解放されない」ことが保証されます。

Q3. ツリーで親子の参照を設計するとき、ベストプラクティスは?

親も子も shared_ptr
親 → 子を shared_ptr、子 → 親を weak_ptr
親も子も weak_ptr
親 → 子を生ポインタ、子 → 親を unique_ptr
両方 shared_ptr だと循環参照でリーク。「親が子を所有(shared)、子は親を参照だけ(weak)」が定石。

Q4. expired()lock() の使い分けとして推奨されるのは?

常に expired() で先にチェック
lock() だけ使い、結果を bool チェック
lock() で shared_ptr を得て nullptr でないか判定(推奨)
両方必須で使う
if (auto sp = wp.lock()) が推奨。expired() の結果と lock() の間に他スレッドがオブジェクトを破棄する可能性があるため、「取れたら使う」を一発で書くのが安全。