閉じる

【C++】shared_from_thisの正しい使い方と注意点を徹底解説

C++でメモリ管理を安全に行うために欠かせないのがstd::shared_ptrです。

しかし、クラスのメンバ関数内から「自分自身のshared_ptr」を生成して外部に渡したい場面では、単純な実装だと致命的なバグを引き起こすことがあります。

そこで登場するのがstd::enable_shared_from_thisです。

本記事では、shared_from_thisの仕組みから正しい使い方、そして開発者が陥りやすい注意点までを詳しく解説します。

非同期処理やイベントリスナーの実装などで必須となる知識ですので、ぜひマスターしてください。

shared_from_thisが必要な理由

C++において、オブジェクトの寿命をstd::shared_ptrで管理している場合、そのオブジェクト自身が「自分へのshared_ptr」を生成する必要があるケースが頻繁に発生します。

例えば、非同期処理のコールバックに関数を登録する際、処理が実行されるまでオブジェクトが消滅しないよう、自分自身の参照を保持させたい時などです。

生ポインタからshared_ptrを生成する危険性

もし、enable_shared_from_thisを使わずに、メンバ関数内でthisポインタから直接shared_ptrを生成してしまうとどうなるでしょうか。

次の図は、その際のメモリ状態のイメージです。

図のように、std::shared_ptr<T>(this)としてしまうと、既存の参照カウンタとは別に新しい参照カウンタ(コントロールブロック)が作成されてしまいます。

その結果、同じオブジェクトに対して複数の独立した管理者が存在することになり、一方がオブジェクトを削除した後にもう一方が削除を試みる「二重解放(Double Free)」が発生し、プログラムがクラッシュします。

これを防ぐために、既存のコントロールブロックを共有しつつ自分へのshared_ptrを取得する仕組みが、shared_from_thisなのです。

enable_shared_from_thisの基本実装

shared_from_thisを利用するには、対象のクラスをstd::enable_shared_from_this<T>から派生させます。

基本的なコード例

以下のコードは、メンバ関数内で自分自身のshared_ptrを生成し、外部の関数に渡す基本的なパターンです。

C++
#include <iostream>
#include <memory>
#include <vector>

// クラスをenable_shared_from_thisから派生させる(CRTPパターン)
class MyClass : public std::enable_shared_from_this<MyClass> {
public:
    void process() {
        std::cout << "処理を開始します。" << std::endl;
        
        // 自分自身のshared_ptrを取得
        // std::shared_ptr<MyClass>(this) と書いてはいけない!
        std::shared_ptr<MyClass> self = shared_from_this();
        
        // 外部に自分自身のshared_ptrを渡す(例:非同期処理のシミュレーション)
        addTask(self);
    }

    void execute() {
        std::cout << "タスクが実行されました!" << std::endl;
    }

private:
    static void addTask(std::shared_ptr<MyClass> ptr) {
        // 本来は非同期キューなどに入れるが、ここでは即時実行
        ptr->execute();
    }
};

int main() {
    // 必須条件:オブジェクト自体をshared_ptrで管理すること
    std::shared_ptr<MyClass> obj = std::make_shared<MyClass>();
    
    obj->process();

    return 0;
}
実行結果
処理を開始します。
タスクが実行されました!

仕組みの解説

std::enable_shared_from_thisを継承すると、クラス内部に「自分を管理しているshared_ptrへのweak_ptr」を保持するための隠れたメンバ変数が追加されます。

CRTP(Curiously Recurring Template Pattern)という手法を用いて、基底クラスに自身の型を渡すことで、適切な型のshared_ptrを返せるようになっています。

使用時の重要な制約と注意点

shared_from_thisは非常に便利ですが、守らなければならない厳格なルールがあります。

これに違反すると、実行時にstd::bad_weak_ptr例外が発生します。

1. オブジェクト自体がshared_ptrで管理されていること

最も多い間違いは、スタック上に生成したオブジェクトや、生ポインタで管理しているオブジェクトに対してshared_from_thisを呼び出すことです。

生成方法shared_from_thisの可否理由
std::make_shared<T>()可能コントロールブロックが作成されるため
std::shared_ptr<T>(new T())可能同上
T obj; (スタック変数)不可能参照カウンタが存在しないため
T* ptr = new T();不可能生ポインタ管理ではカウンタが作成されない

2. コンストラクタ内では呼び出せない

クラスのコンストラクタ内でshared_from_thisを呼び出すことはできません。

なぜなら、コンストラクタが実行されている時点では、まだそのオブジェクトを管理するshared_ptrの構築が完了していないからです。

C++
class Forbidden {
public:
    Forbidden() {
        // 例外 std::bad_weak_ptr が発生する!
        auto self = shared_from_this();
    }
};

回避策:Factoryメソッドパターンの利用

コンストラクタ直後に何か登録作業を行いたい場合は、コンストラクタをprivateにし、静的な生成用メソッド(Factoryメソッド)を用意するのが一般的です。

C++
class SafeClass : public std::enable_shared_from_this<SafeClass> {
private:
    SafeClass() {} // コンストラクタを隠蔽

public:
    static std::shared_ptr<SafeClass> create() {
        auto ptr = std::shared_ptr<SafeClass>(new SafeClass());
        ptr->init(); // ここで初期化処理
        return ptr;
    }

    void init() {
        auto self = shared_from_this(); // ここなら安全!
        std::cout << "初期化に成功しました。" << std::endl;
    }
};

C++17以降で追加された便利な機能

モダンなC++(C++17以降)では、より安全で柔軟な管理ができるよう機能が追加されています。

weak_from_this() の活用

C++17からは、weak_from_this()というメンバ関数が追加されました。

shared_from_this()は呼び出した瞬間に参照カウンタを増やしますが、weak_from_this()はカウンタを増やさずに「自分への弱参照」を取得します。

主な用途:

  • 循環参照を防止したいとき。
  • オブジェクトが生存している場合のみ処理を行いたいキャッシュ機構の実装。
C++
void asyncTask() {
    // 参照を増やさずに自身の弱参照を取得
    std::weak_ptr<MyClass> weakSelf = weak_from_this();

    // 別のスレッド等で実行
    if (auto self = weakSelf.lock()) {
        // オブジェクトがまだ生きていれば実行
        self->execute();
    }
}

継承関係における注意点

大規模な開発において、クラスを継承した際にもshared_from_thisを使いたい場合があります。

ここで注意が必要なのが「多重継承」や「継承の深さ」です。

多重継承による衝突

複数の基底クラスがそれぞれstd::enable_shared_from_thisを継承している場合、派生クラスでshared_from_thisを呼び出すと曖昧さが発生し、コンパイルエラーや予期せぬ動作の原因となります。

これを避けるには、継承ツリーの最上位の基底クラスでのみenable_shared_from_thisを継承するように設計します。

派生クラスでの型変換

基底クラスでenable_shared_from_this<Base>としている場合、shared_from_this()が返す型はstd::shared_ptr<Base>になります。

派生クラスの型として扱いたい場合は、std::static_pointer_castを使用します。

C++
class Base : public std::enable_shared_from_this<Base> {
public:
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    void doSomething() {
        // Baseのポインタが返るのでキャストが必要
        std::shared_ptr<Derived> self = std::static_pointer_cast<Derived>(shared_from_this());
    }
};

まとめ

std::enable_shared_from_thisは、オブジェクト指向プログラミングにおいてオブジェクトの寿命を安全に管理するための強力なツールです。

本記事のポイント:

  • thisから直接shared_ptrを作ると二重解放の原因になる。
  • 必ずクラスをpublic std::enable_shared_from_this<T>から派生させる。
  • オブジェクト自体をshared_ptrで生成・管理していることが利用の絶対条件。
  • コンストラクタ内での呼び出しは厳禁。Factoryメソッドを利用する。
  • C++17以降ならweak_from_this()も活用して柔軟な設計を。

これらを意識することで、メモリリークや不正アクセスといった低レイヤーのバグを未然に防ぎ、堅牢なC++プログラムを記述することができます。

特に非同期処理を多用する現代のアプリケーション開発においては必須のテクニックですので、この機会に正しく理解しておきましょう。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!