第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 の挙動
use_count = 0
weak_count = 0
ヒープ上のオブジェクト
最小コード
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 方向、弱い)
// → スコープ終了で正しく解放
判断基準
- 親 → 子:親が子を所有 →
shared_ptr(または子が固有なら unique_ptr)
- 子 → 親:子は親を参照だけ →
weak_ptr
- ノード間の兄弟リンク:所有者が別にいる →
weak_ptr
ツリー構造の定番:
- 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() の間に他スレッドがオブジェクトを破棄する可能性があるため、「取れたら使う」を一発で書くのが安全。