std::enable_shared_from_this はプライベートで可変の std::weak_ptr<T> メンバー(通常は weak_this と名付けられます)をカプセル化するミキシンベースクラスです。派生オブジェクトの構築中、この内部 weak_ptr はデフォルト構築され、空(期限切れ)状態になります。この重要なアーキテクチャの詳細は、この内部ポインタの初期化が管理されているオブジェクトのコンストラクタが完了した後にのみ、std::shared_ptr コンストラクタ内で制御ブロックを参照するために行われるということです。したがって、コンストラクタ本体内で shared_from_this() を呼び出すと、空の weak_ptr に対して lock() を呼び出そうとし、C++17 以降では std::bad_weak_ptr 例外をスローすることが要求されます(以前の標準では未定義の動作となります)。これは、新しい参照を提供するために必要な共有所有権のインフラストラクチャがまだ確立されていないためです。
コンテキスト:
高頻度取引プラットフォームが MarketDataHandler クラスを実装し、株式市場への持続的なTCP接続を管理しました。非同期ソケットの読み取り/書き込み操作中にハンドラが生存し続けることを保証するために、このクラスは std::enable_shared_from_this<MarketDataHandler> から継承されました。コンストラクタは接続パラメータを受け取り、すぐに非同期読み取り操作を開始し、完了ハンドラとして shared_from_this() を Boost.Asio イベントループに渡しました。
問題:
統合テスト中に、接続の確立時にアプリケーションがすぐにクラッシュし、未捕捉の std::bad_weak_ptr 例外がプロセスを終了させました。開発チームは、基本クラス std::enable_shared_from_this のサブオブジェクトが派生クラスのコンストラクタ本体が実行される前に構築されるため、内部トラッキングメカニズムが即座に使用可能になると仮定していました。彼らは、オブジェクト構築と std::shared_ptr ラッパーの完了との間の時間的なギャップを考慮し忘れ、ファクトリー式が終了するまで内部 weak_ptr が初期化されないままであることに気づきませんでした。
考慮された代替ソリューション:
post_construct()を使った二段階初期化:
クラスをリファクタリングし、非同期初期化ロジックをコンストラクタから別の post_construct() パブリックメソッドに移動します。呼び出し元はまず std::make_shared<MarketDataHandler> を使用して std::shared_ptr<MarketDataHandler> を作成し、すぐにその結果に対して post_construct() を呼び出してからポインタをシステムに返します。
post_construct() の呼び出しを忘れ、ハンドラがデータ処理を開始しないという微妙なバグを引き起こす可能性があります。外部ライフタイム保証の生ポインタ:
生の this ポインタを非同期I/Oシステムに渡し、std::shared_ptr キーを使用してアクティブな接続の別のグローバルレジストリを維持し、すべてのコールバック実行時にレジストリメンバーシップを確認します。
shared_from_this() を必要としません。プライベートコンストラクタを持つ静的ファクトリーメソッド:
すべてのコンストラクタをプライベートにし、std::shared_ptr<MarketDataHandler> を返すパブリックな静的 create() メソッドを提供します。create()の内部で、最初に std::make_shared を使用してオブジェクトを構築し、次に結果の共有ポインタを使用して非同期操作を開始し、それを呼び出し元に返します。
std::make_shared の使用を防ぎます; やや冗長な構文(MarketDataHandler::create() 対 std::make_shared<MarketDataHandler>())が必要です。選択された解決策:
静的ファクトリーパターンが選択され、所有されていないオブジェクトで shared_from_this() を呼び出す可能性が排除されました。create() メソッドに建設を制限することで、いつでも std::shared_ptr 制御ブロックが完全に構築され、任意のメソッドが追加の参照を発行しようとする前に内部 weak_ptr が初期化されることを保証しました。
結果:
リファクタリングはすべてのスタートアップクラッシュを排除しました。このコードベースはネットワーキングレイヤー全体で一貫して適用される堅牢な非同期オブジェクト作成パターンを採用しました。コードレビューガイドラインはファクトリー構築後に呼び出されるメソッド外での shared_from_this() 呼び出しを禁止するように更新され、ライフタイム関連の欠陥率が大幅に減少しました。
質問: shared_from_this() は参照カウントを増加させますか?制御ブロックとはどのように相互作用しますか?
回答:
shared_from_this() は新しい制御ブロックを作成しません。代わりに、std::enable_shared_from_this ベースクラス内に保存された内部の可変 std::weak_ptr<T> メンバーにアクセスし、lock() を呼び出します。この操作は、制御ブロックがまだ存在するかどうかを原子的に確認し、存在する場合は既存の制御ブロックに関連する強い参照カウントをインクリメントし、所有権を共有する新しい std::shared_ptr インスタンスを返します。オブジェクトが既に破棄されている(期限切れの弱いポインタ)場合、lock() は空の std::shared_ptr を返します。候補者はしばしば shared_from_this() が内部の shared_ptr のコピーを返すだけだと誤解しており、実際には弱い参照を強い参照に昇格させることを見逃していますが、これは二重所有権のシナリオを回避するために重要です。
質問: クラスは std::enable_shared_from_this<T> から複数回継承できますか?それともダイヤモンド階層内の異なるパスからですか?
回答:
クラスは同じ T のために std::enable_shared_from_this<T> から直接複数回継承することはできません。なぜなら、それは曖昧な基底クラスのサブオブジェクトを生み出すからです。しかし、Derived クラスは専ら std::enable_shared_from_this<Derived> から継承すべきであり、基底クラスのバージョンからではありません。候補者が見逃す重要な詳細は、仮想継承に関することです:もし Base が std::enable_shared_from_this<Base> から継承し、Derived が Base から継承すると、Derived 内の Base ポインタ上で shared_from_this() を呼び出すと正しく動作します。なぜなら、内部 weak_ptr が最も派生したオブジェクトを指すように初期化されるからです。しかし、Derived が std::enable_shared_from_this<Derived> からも公開で継承すると、これは二つの異なる weak_ptr メンバーを生じ、どちらが初期化されるのかについての混乱を引き起こします。標準は、std::shared_ptr コンストラクタによる初期化が特に std::enable_shared_from_this の特殊化を探すことを義務づけています; 複数の独立した weak_ptr メンバーを持つことは、通常は最初に std::shared_ptr を作成するために使用される静的タイプに関連付けられたものが一つだけを初期化し、他のものは空のままとなり、後続の shared_from_this() 呼び出しが失敗する原因となります。
質問: std::make_shared と std::shared_ptr<T>(new T) の違いは、コンストラクタ中の shared_from_this() の安全性には無関係ですか?
回答:
両方の割り当て戦略は最終的に std::shared_ptr コンストラクタを呼び出し、テンプレートメタプログラミングを通じて std::enable_shared_from_this ベースクラスを検出します。内部 weak_ptr の初期化は、new T の実行中や make_shared の内部オブジェクト構築フェーズ内ではなく、std::shared_ptr コンストラクタロジック内でのみ行われます。具体的には、make_shared はストレージを割り当て、T オブジェクトを構築し(この間、weak_ptr は空のまま)、その後に std::shared_ptr コンストラクタが weak_ptr を新しく作成された制御ブロックを指すように初期化します。候補者はしばしば、make_shared がその単一割り当ての最適化によりオブジェクトを早く「準備」する可能性があると誤解しますが、標準はどのファクトリ関数を使用してもコンストラクタ本体からの shared_from_this() の呼び出しが安全でないことを保証しています。なぜなら、weak_ptr の代入が T のコンストラクタの完了後の厳密な時に行われるからです。