この記事のポイント

  • 循環参照がなぜ発生するかを設計責務で理解できる
  • shared_ptr/weak_ptrの切り分けをレビューアー視点で整理
  • 循環参照の「起点と断ち切り役割」を設計から読み解く

そもそも循環参照とは

C++のshared_ptrは参照カウントでオブジェクトの寿命を管理します。
複数のshared_ptrが互いに所有権を持ち合うと、以下のような循環参照(サイクル)が発生します。

A ---> B
^      |
|      v
+------+

このとき、参照カウントはゼロにならず、誰もオブジェクトを解放できなくなります。
C++のshared_ptrはGC言語ではないため、循環参照はメモリリークの主因になります。

  • shared_ptrは「複数所有」を許すが「相互所有」は許さない
  • weak_ptrが断ち切り役
  • 循環参照は設計問題であり実行時問題ではない

なぜこれをレビューするのか

レビュー段階で循環参照のリスクを摘み取れなければ、実行時に気付きづらいリークが残ります。
しかもC++の循環参照は

  • 長期間気付かれない
  • 原因追跡が極めて困難
  • 責務分界崩壊の兆候

という重大設計事故につながりやすいです。

レビューアー視点

  • 双方向参照が存在しないかを常に読む
  • 強参照(shared_ptr) vs 弱参照(weak_ptr)の責務線を読む
  • 所有権の主従関係をコード上で確認する
  • 断ち切り役を誰が担うのか確認する

開発者視点

  • 「上位が強参照・下位が弱参照」を基本原則に置く
  • 双方向参照禁止が設計原則になる
  • 子→親参照では必ずweak_ptrを使う習慣化
  • フレームワーク内の循環も明示的にレビューする

良い実装例

想定:親子構造の片方向強参照・片方向弱参照パターン

良い設計例
#include <memory>
#include <vector>
#include <iostream>

struct Child; // 前方宣言

struct Parent : public std::enable_shared_from_this<Parent> {
    std::vector<std::shared_ptr<Child>> children;
};

struct Child {
    std::weak_ptr<Parent> parent;  // 弱参照にすることで循環を防止
};

void buildHierarchy() {
    auto parent = std::make_shared<Parent>();

    auto child1 = std::make_shared<Child>();
    child1->parent = parent;

    auto child2 = std::make_shared<Child>();
    child2->parent = parent;

    parent->children.push_back(child1);
    parent->children.push_back(child2);
}

良いポイント

  • 所有者は親(shared_ptr保持)
  • 従属先は子(weak_ptr保持)
  • 循環経路が断ち切られ設計責務が自然

レビュー観点

  • 双方向参照の有無をまず確認
  • 所有権主従が正しく整理されているか
  • 子→親参照はweak_ptrか
  • enable_shared_from_thisの適用妥当性は検証済みか
  • ユースケース拡張時に循環経路が発生し得ないか

良くない実装例: ケース1(双方向強参照)

問題例①
#include <memory>
#include <vector>

struct Parent;
struct Child;

struct Parent {
    std::vector<std::shared_ptr<Child>> children;
};

struct Child {
    std::shared_ptr<Parent> parent; // 循環発生
};
@Reviewer
双方向にshared_ptrを持ち合う設計は循環参照を招きます。Child側はweak_ptrで持ち、親が唯一の所有者になるよう修正してください。

問題点

  • 互いが所有権を持つため参照カウントがゼロにならない
  • deleteされずリーク化
  • 意図しない寿命延長事故が起こる

改善例

改善例①
struct Child {
    std::weak_ptr<Parent> parent; // weak_ptrに変更し断ち切る
};

良くない実装例: ケース2(enable_shared_from_this未使用)

問題例②
struct Parent {
    void addChild(std::shared_ptr<Child> child) {
        child->parent = shared_from_this(); // これが使えない
    }
};
@Reviewer
shared_from_thisを使用するためにはenable_shared_from_this継承が必要です。Parentをenable_shared_from_this<Parent>継承に修正してください。

問題点

  • shared_from_this()を正しく使えていない
  • 自インスタンスをweak_ptrから再度取得できない
  • 寿命管理がブレる

改善例

改善例②
struct Parent : public std::enable_shared_from_this<Parent> {
    void addChild(std::shared_ptr<Child> child) {
        child->parent = shared_from_this();
    }
};

良くない実装例: ケース3(断ち切り役の責務未定義)

問題例③
class SessionManager {
public:
    void setSession(std::shared_ptr<Session> session) {
        currentSession = session;
        session->manager = shared_from_this();
    }

private:
    std::shared_ptr<Session> currentSession;
};
@Reviewer
SessionがSessionManagerをshared_ptrで参照すると完全循環になります。Session内のmanagerはweak_ptrで持つよう修正してください。

改善例

改善例③
class Session {
public:
    std::weak_ptr<SessionManager> manager;
};

観点チェックリスト


まとめ

循環参照防止レビューはスマートポインタ設計レビューの中でも最重要課題です。
レビューアーはコード構造を静的に読み取り、どこに循環が発生し得るかを常に逆算して確認する必要があります。

設計段階で読み取る習慣さえ身につけば、実行時に困ることは激減します。

UML Diagram