6.2 Singleton をサポートする C 十十の基本的イディオム 139 このアプローチにおけるもうーっの些細な問題は , 初期化とクリーン・アップが難しい点です。つまり , My0n1yPrinter のデータを初期化 , クリーン・アップするための中心点が存在しないのです。初期化やクリー ン・アップは , 簡単な作業ではありません。例えば , defau1tFont- は p て intingport ーの速度に依存する可能性 があるのです。 従って Singleton の実装では , 2 番目のインスタンスを生成しないようにしながら , オプジェクトの生成と唯 一性の管理に集中することになるわけです。 6.2 Singleton をサポートする C 十十の基本的イディオム 0 + では通常の場合 , 以下のようなイディオムを用いて SingIeto Ⅱを実装します : new SingIeton; private : . 操作 return plnstance— plnstance— if (!plnstance—) static Sing1eton* lnstance() / / 唯一のアクセス・ポイント public : class Sing1eton / / ヘッダ・ファイル Sing1eton. h class Sing1eton / / ヘッダ・ファイル SingIeton. 単純化できると思われるかもしれませんが , その考えは間違っています : 上の例にあった p 工 nstance ーをポインタではなく , Sing1eton オプジェクトそのもので置き換えれば , 実装を Sing1eton の生成が高価で , かっ滅多に使われないものである場合に有効となります。 判定です ( 通常の場合は無視できるものです ) 。最初の要求があった時点で生成を行うという解決策の利点は , 合 ) , Sing1eton オプジェクトは生成されません。この最適化から来る代償は , lnstance の先頭で行われる S ⅲ gleton デザイン・パターンがまったく使用されない場合 ( つまりエ nstance の呼び出しが発生しない場 で S ⅲ gleto Ⅱデザイン・パターンを実装する際の工ッセンスです。 す。このため , Sing1eton オプジェクトの唯一性をコンパイル時点で保証できるようになります。これが C+ + きません。 Si Ⅱ glet 。 n 自身のメンバ関数 , つまりエ nst ce だけがオプジェクトの生成を許されているわけで 全てのコンストラクタは private となっているため , ユーザ・コードによって Sing1eton を生成することはで Sing1eton* Sing1eton: : p 工 nstance— / / ファイル Sing1eton ・ cpp における実装 static SingIeton* plnstance— ; / / 唯一のインスタンス格納域 / / コピー生成を抑止する Sing1eton(const Sing1eton&) ; / / 利用者側による Sing1eton の Sing1eton() ; / / 利用者側による新たな Si Ⅱ glet 。 n 生成を抑止する
6.5 死んだ参照の問題 if ( !plnstance—) / / 死んだ参照のチェック if (destroyed-) OnDeadReference ( ) ; else / / 最初に呼び出された際の初期化 Create() ; return plnstance— virtual &Sing1eton() throw std: : runtime—error("Dead Reference Detected" ) ; static void OnDeadReference ( ) / / 死んだ参照を検出した場合 , 呼び出される &thelnstance ; plnstance— static Sing1eton thelnstance ; / / 作業 : p 工Ⅱ st ce ーの初期化 static void Create() / / p 工 nstance- に格納する / / 新たな Sing1eton を生成し , そのポインタを private : 145 plnstance— destroyed- / / データ true ; Sing1eton plnstance— b001 destroyed— 各種コンストラクタ / テストラクタ , operator= の抑止 / / Sing1eton. cpp Sing1eton* Sing1eton: :plnstance— b001 Sing1eton: : destroyed- false; ジェクトが , 破棄後に Singleton へのアクセスを行おうとした場合 , 制御フローは OnDeadReference に移り , デストラクタでは , plnstance- がゼロに , destroyed- が true に設定されます。これよりも寿命の長いオプ ひとまず完成です ! アプリケーションが終了すると同時に , Sing1eton のデストラクタが呼び出されます。
6.4 Singleton の破棄 141 ションに置くことによって , 自動生成は抑止され , sneaky の定義をコンパイル時のエラーにすることができる のです。 もう 1 つの小さな改善は , lnst ce がポインタではなく参照を返すようにすることです。工 nstance がポイ ンタを返す場合 , 呼び出し側によって delete されてしまう危険性が考えられます。こういった事態を低減でき るよう , 参照を返すわけです : / / Sing1eton クラスの内側 static Sing1eton& lnstance ( ) ; コンパイラによって暗黙のうちに生成されるもう 1 つのメンバ関数として , 代入演算子があります。オプジェ クトの唯一性が代入と直接関係することはありませんが , 唯一性の保証によって , オプジェクト間の代入とい うものがあり得ない ( オプジェクトが複数存在しないため ) という明らかな結論を導き出すことができます。 Si Ⅱ glet 。Ⅱオプジェクトにとって , 代入とは意味のない自己代入しかあり得ないわけです。従って , 代入演算 を抑止する (private にして , 実装しておかない ) 価値があるのです。 そして最後のお守りは , デストラクタを private にすることです。この方法によって , s 土 ngleton オプジェ クトへのポインタを保持している利用者側が , 誤ってそれを削除することを防ぐわけです。 こまでで挙げた手段を講じると , Sing1eton インタフェースは以下のようになります : class Sing1eton Sing1eton& lnstance ( ) ; . 操作 . private : Sing1eton() ; Sing1eton(const Sing1eton&) ; Sing1eton& operator=(const Sing1eton&) ; &Sing1eton() ; 6 4 Singleton の破棄 今まで考察してきた通り , Sing1eton は 1 Ⅱ stance が最初に呼び出された際 , 必要に応じて生成されます。 つまり , 構築のタイミングは工 nstance への最初の呼び出し時と定義できます。しかし , 破棄については考察さ れていません。 SingIeton のインスタンスは , いつ破棄すれば良いのでしようか ? GoF 本はこの問題について 触れていませんが , John VIissides の書籍 Pattern 丑観 c 厖 ( 1998 ) では , この問題が困難なものであること を述べています。 実際 , Sing1eton が削除されなくても , それはメモリ・リークにはなりません。メモリ・リークは , 何度も こういったも データが割り当てられ , その全ての参照が失われるような場合に表面化します。 こでの問題は , のではありません。データがいくつも割り当てられるわけではなく , 割り当てられたメモリに関する知識もアプ リケーションの終了まで保持されているのです。さらに , 今日のオペレーティング・システムは全て , プロセス の終了に際してメモリの解放を行ってくれるはずです。 ( 何がメモリ・リークで , 何がそうでないかについての 面白い考察については , Effective C+ + (Meyers1998a) の項目 10 を参照して下さい。 )
140 public : static Sing1eton* 工 nstance ( ) return &instance_ int DoSomething() ; private : static SingIeton instance— 第 6 章 SingIeton の実装 / / 唯一のアクセス・ポイント / / ファイル Sing1eton. cpp における実装 SingIeton SingIeton: : instance— (Meyers1998a)0 このため , 明示的なコピー・コンストラクタを宣言し , そのコンストラクタを private セク ・コンストラクタが定義されていない場合 , コンパイラは最も妥当なものを public 扱いで定義します コピー / / 'sneaky' を ( Sing1eton ) オブジェクトのコピーにすることはできない / / lnstance を返すことによって / / 工ラー ! Sing1eton sneaky(*Sing1eton : : lnstance ( ) ) ; す。後者によって , 以下のようなコードをエラーにすることができます : にしてきました。それらは , テフォルト・コンストラクタとコピー・コンストラクタを private にすることで Singleton の唯一性を保証するために利用できる言語上のテクニックがいくつかあります。既にいくつかは目 6.3 Singleton の唯一性を保証する が初期化されていることを期待できないのです。 は未だ構築されていないオプジェクトを返す場合があるわけです。つまり , 他の外部オプジェクトは , instance- コンパイラが , instance- や global の初期化をどのような順序で行ったのかによって , Sing1eton: :lnstance int global Sing1eton: : lnstance ()—>DOSomething() ; #include "Sing1eton. h" / / SomeFi1e . cpp すればコンパイル可能な 0 + のソースファイルのことです。 ) 以下のコードを考察してみましよう・ の初期化順序を定義していないため , 大きな問題の温床を作り出しているのです。 ( 翻訳単位とは , 乱暴に表現 初期化はロード時に行われることになります。 ) 一方 , C+ + は異なった翻訳単位における動的初期化オプジェクト うに準備します。 ( 通常の場合 , 静的初期化子は実行可能プログラムを保持したファイル中に記述されるため , コンパイラは , プログラムを構成する最初のアセンプリ言語の実行に先だって , 静的な初期化が実行されるよ パイル時の定数として初期化されるコンストラクタのない型です ) 。 (Sing1eton のコンストラクタが実行時に呼び出されます ) , insta れ ce ーは静的に初期化されます ( これは , コン と同じ ) なのですが , この 2 つのバージョンには重大な違いがあるのです。 p 工 nstance- は , 動的に初期化され これは良い解決策ではありません。 instance- も Sing1eton の static メンバ ( 前の例における plnstance-
6.5 死んだ参照の問題 / / Sing1et 。 n を保持するバッファ / / ( これは正しく整列されていると仮定しています ) static char —-buffer [sizeof (Sing1eton)] ; if ( ! ——initialized) / / 初回呼び出し。構築されたオブジェクトは / / --buffer メモリ中の Sing1eton: :Sing1eton を起動する —ConstructSing1eton(——buffer) ; / / 破棄の登録 atexit (—DestroySing1eton) ; _initialized = true ; return *reinterpret—cast く Sing1eton *>(——buffer) ; 143 こでの核となる部分は , atexit 関数の呼び出しです。標準 C ライプラリが提供する atexit 関数は , プロ グラムの終了時に後入れ先出し (LIFO) 順で自動的に呼び出される関数を登録するものです。 ( C+ + におけるオ プジェクトの破棄は定義により , 最初に生成されたオプジェクトを最後に破棄する , LIFO 形式で行われます。 もちろんあなた自身が new と delete を用いて管理するオプジェクトは , この規則に従う必要はありません。 ) atexit のシグネチャは以下の通りです : / / 関数へのポインタを受け取り , 成功した場合は 0 , / / 工ラーが発生した場合は非ゼロを返します。 int atexit (void (*pFun) ( ) ) ; --buffer のメモリ中に格納されている Sing1eton オプジェクトの破棄を行う関 コンパイラは , 数 --DestroySing1eton を生成し , Sing1eton オプジェクトの生成時にその関数のアドレスを atexit に 引き渡すコードを生成するわけです。 atexit は , どのように動作するのでしようか ? atexit を呼び出す度に , C ランタイム・ライプラリが管理 するプライベート・スタックに , そのパラメータがブッシュされます。そして , アプリケーションの終了処理で は , atexit によって登録された関数がランタイム・サポートによって順に呼び出されるわけです。 この後すぐに見ていただきますが , atexit, および 0 + による Singleton デザイン・パターンの実装との間に は重要な ( そして時折 , 不幸な ) 関係が存在します。好むと好まざるに関わらず , この章の終わりまでそれはつ きまといます。どのような解決策で S ⅲ glet 。Ⅱの破棄を行うとしても , atexit とはうまく連携する必要があり , それを怠るとプログラマの期待を裏切る結果が待ち受けているのです。 Meyers の Singleton は , アプリケーションの終了処理における最も簡単な Singleton の破棄手段を提供して 質を備えた例です。 う。これは , 表現しやすく理解しやすいものの , 実装することが難しいという Singleton パターン自身と同じ性 様々な実装を検証する際 , より具体的に考察できるよう , 6.5 死んだ参照の問題 における代替実装を考えてみましよう。 います。これはほとんどの場合でうまく動作します。では , その問題を考察し , いくつかの改善と特殊なケース この章の残りを通じて用いる例を決めておきましょ
6.6 死んだ参照の問題 ( ) : Phoenix Singleton void Sing1eton: : Ki11PhoenixSing1eton() / / 全ての灰を作成し , 手作業でデストラクタを呼び出す / / これによって plnstance- がゼロに , destroyed- が true に / / 設定される p 工 nstance——>NSing1eton() ; 147 OnDeadReference が使用している new 演算子は , new の配置構文 (placement new operator) と呼ばれてい ます。 new の配置構文はメモリを割り当てず , 引き渡されたアドレスーこの場合 p 工 nsta れ ce ーに新たなオプジェ クトを構築するだけです。 ne の配置構文に関する興味深い考察については , Meyers ( 1998b ) を参照されると 良いでしよう。 上述の Sing1eton では , 新たなメンバ関数 Ki11PhoenixSingIeton が追加されました。これは , Phoenix Singleto Ⅱを復活させるためにⅡ ew を使用すると , static 変数に対して行われていたような , コンパイラの魔 法による自動破棄が行われなくなるためです。つまり , 手作業で構築したものは , 手作業で破棄しなけらばなら ないのです。このため , atexit(Ki11PhoenixSing1eton) によってそれを保証するわけです。 では , イベントの流れを分析してみましよう。アプリケーションの終了手順中に , Sing1et 。 n のデストラクタ が呼び出されます。デストラクタは , ポインタをゼロにリセットし ,destroyed- を true に設定します。 どこかのグローバル・オプジェクトがもう一度 Sing1eto Ⅱへのアクセスを試みたと考えて下さい。工 nstance は OnDeadReference を呼び出します。 OnDeadReference は , SingIeton を復活させ , Ki11PhoenixSing1eton への呼び出しを登録し , 工Ⅱ stance は無事に有効な Sing1eton オプジェクトへの参照を返すことになるわけで す。以降も , 同じサイクルが繰り返されます。 PhoenixSingleton クラスを用いることによって , グローバル・オプジェクトや他の S ⅲ gleto Ⅱクラスに対 して , 常に有効なインスタンスを提供できることが保証されます。このため PhoenixSingleton は , Log のよ うな問題を解決できる , 堅牢な全方位オプジェクトを実現する魅力的な解決策となるのです。 Log を Phoenix S ⅲ gleto Ⅱにした場合 , どのような順序でエラーが発生しても , プログラムは正しく動作することになります。 6.6.1 atexit の問題 前のセクションにおけるコードと Loki における実際のコードを比較していただくと , 違いがあることに気付 かれると思います。つまり , atexit の呼び出しは , プリプロセッサ指令 # ifdef に囲まれているのです : #ifdef ATEXIT—FIXED / / 新たなオブジェクトのデストラクタを登録する atexit (KiIIPhoenixSing1eton) ; #endif #define ATEXIT-FIXED を記述していない場合 , 新たに生成される Phoenix Singleton は破棄されず , 私たち が避けようと格闘しているリークが発生してしまうわけです。 これは 0 + 規格における残念な手抜きに対処するためのものなのです。規格では , atexit を用いた関数の登 録中に他の atexit による登録が発生した場合の定義が行われていないのです。 この問題を明確にするため , 小さなテスト・プログラムを考察してみましよう :
142 第 6 章 Singleton の実装 しかし , リークは存在するのです。そして , それはずっと陰険なもの , つまりリソース・リークというものな のです。 Sing1eton のコンストラクタでは , ネットワーク接続 , OS 全体にまたがるミューテックスのハンド ル , その他のプロセス間通信手段 , CORBA オプジェクトや COM オプジェクトによって生成される参照といっ た , 任意のリソース群を獲得することができるのです。 リソース・リークを避ける唯一の正しい手段とは , アプリケーションの終了時に Si Ⅱ glet 。Ⅱオプジェクトを 削除することです。問題は , その破棄後に , 該当 Si Ⅱ glet 。 n に対するアクセスが発生しないようなタイミング を注意深く設定しなければならない点です。 Singleton の破棄に対する最も単純な解決策は 旨のメカニズムに頼ることです。例えば , 以下のコード にコロロ は Singleto Ⅱを異なったアプローチで実装したものです。動的割り当てと static ポインタを用いる代わりに lnstance 関数は静的局所変数 ( あ c s ね翫勧いを使用しています : Sing1eton& Sing1eton: : lnstance() static Sing1eton Obj ; return Obj ; この単純でエレガントな実装は Scott Meyers によって最初に公表されました (Meyers 1996a , 項目 26 ) 。従っ て , 以降これを「 Meyers の Singleton 」と呼ぶことにします。 Meyers の Singleton は , コンパイラが提供して いる , ある種の魔法を利用しています。関数内の static オプジェクトは , 制御フローがその定義に初めて差し 掛かった際に初期化されます。コンパイル時定数によってプリミテイプな初期値が設定されている静的変数を用 いて , 実行時に初期化が行われる stat i c 変数と混同してはいけません。例えば : int Fun() static int x = 100 ; return 十十 X ; この場合 , x はこのプログラムにおける任意のコードが実行される前に初期化されます。このため Fun が最初に 呼び出されるずっと前から , x は 100 となっています。これに対し , 初期化子がコンパイル時の定数でない場 合 , あるいは static 変数がコンストラクタを伴うオプジェクトである場合 , その変数は実行時に制御フローが 初めて該当定義に差し掛かった際に初期化されます。 加えて , 初期化後に実行時サポートが該当変数を破棄対象として登録するようなコードが , コンパイラによっ て生成されます。生成されるコードを疑似 0 + 表現で記述すると以下のようになります ( 2 つのアンダースコ アで開始される変数は , 見えないもの , すなわちコンパイラによって生成 , 管理される変数であると考えて下 さい ) : Sing1eton& Sing1eton: :-lnstance ( ) —initialized = false; / / コンパイラによって生成された変数 —DestroySig1eton() ; —ConstructSing1eton(void* memory) ; / / コンパイラによって生成された関数 static b001 extern void extern VOid
ru Ⅱ time ー e てて or 型の例外カゞスローされるのです。この解決策は , 言えます。 146 第 6 章 Singleton の実装 コストが掛からず , 単純で , 効果的なものと 6.6 死んだ参照の問題 ( ) : Phoenix Singleton KDL (Keyboard, Disp1ay, Log) 問題に対して前のセクションで採用した解決策を適用しても , 結果は満 足のいくものとはなりません。 Log が破棄された後に Disp1ay のデストラクタがエラーを報告しようとした場 合 , Log : : lnstance は例外をスローしてしまうのです。私たちは未定義の動作となることを防ぎましたが , 今 度は満足できない動作に直面するわけです。 私たちにとっては , 構築されているかどうかとは関係なく , いつでも Log が利用可能になっていて欲しいの です。極端な話 , L 。 g が破棄されていたとしても , もう一度生成することができれば , いつでもエラー報告のた めに使用することができるわけです。これが Phoenix Singleton デザイン・パターンの背景にあるアイディア です。 伝説の不死鳥フェニックス ( P ん。印れ ) が自らの灰から復活するように , Phoenix SingIeton も破棄後に復活す ることができるのです。 Sing1eton オプジェクトの特徴であるインスタンスの唯一性は , 常に保証される ( 同時 に 2 つの SingIet 。 n が存在することは無い ) 上 , 死んだ参照を検出した場合 , インスタンスを再生成することも できるのです。 Phoenix Singleton パターンを使えば , 簡単に KDL 問題を解決することができます。 Keyboard と Disp1ay は「通常の」 Singleton であり , Log は Phoenix Singleton となるわけです。 static 変数を用いた Phoenix Singleton の実装は簡単です。死んだ参照を検出した際 , 新たな Sing1eton オプジェクトを古い抜け殻の中に生成するのです。 ( 0 + は , こういったことが可能であることを保証して います。 static オプジェクトのためのメモリ領域は , プログラムの実行中ずっと残っているのです。 ) そし て , この新たなオプジェクトの破棄を atexit を用いて登録します。工 nstance を修正する必要はありません。 OnDeadReference プリミテイプを修正するだけで良いのです : class Sing1eton . 上記と同じ void Ki11PhoenixSing1eton() ; / / 追加 void Sing1eton : : OnDeadReference ( ) / / 破棄された Si Ⅱ glet 。Ⅱの抜け殻を獲得する Create() ; / / これで plnstance- は SingIeton の「灰」 , つまり / / Sing1et 。 n が格納されていた生の (raw) メモリを指します new(p 工 nstance—) Sing1eton; / / 新たなオブジェクトの破棄を登録する atexit (Ki11PhoenixSing1eton) ; / / 復活にともない , destroyed- を再設定する destroyed— = false ;
6.9 マルチスレッド対応 6.9 マルチスレッド対応 のスレッドで構成されたアプリケーションが開始したと考えて下さい・ Singleton は , マルチスレッド環境下でも扱えなければなりません。以下の Si Ⅱ gleton にアクセスする , 155 2 つ Sing1eton& Sing1eton: : lnstance ( ) if (!plnstance-) p 工 nstance— return *plnstance_ new SingIeton; / / 1 / / 2 / / 3 最初のスレッドが工 nstance に入り , if 条件をテストします。これは , pl Ⅱ st ce ーに対する初めてのアクセス となり , null と判定されるため , スレッドは / / 2 と記された行に到達し , Ⅱ ew 演算子を起動する準備が行われ ます。この時点で OS のスケジューラが , 最初のスレッドに割り込みを行い , 他のスレッドに制御を引き渡す場 合があります。 そして 2 番目のスレッドに制御が渡された場合 , Sing1eton: 工 nstance() の起動が実行されます。最初のス レッドは p 工 nstance ーの変更をまだ行っていないため , 同じように nu Ⅱであると判定されます。この時点では , 最初のスレッドは p 工 nstance- をテストしただけなのです。そして , 2 番目のスレッドが new 演算子を呼び出 し , p 工 nst ce ーに値を代入し , そのままこの関数を終了したと考えて下さい。 不幸なことに , 最初のスレッドに制御が戻ってくると , それはちょうど / / 2 の行を実行しようとしていたと ころであったため , pl Ⅱ st ce ーに再度代入を行い , 関数を終了します。一連の処理が終わった後には , 1 つでは なく 2 つの Sing1eton オプジェクトが存在することになるため , そのうちの 1 つは確実にリークするわけです。 そして , それぞれのスレッドには , Sing1eton の異なったインスタンスが保持され , アプリケーションは破滅 に向かって真っ直ぐに突き進んでいくのです。しかも , これは考えられるシナリオの 1 つでしかありません一複 数のスレッドがその SingIeton に対して群がってアクセスするとどうなるのでしようか ? ( ちょっと考えてみ て , これをデバッグしてみて下さい。 ) 経験の深いマルチスレッド・プログラマは , これが古典的な競合条件であることをご存じでしよう。 Singleton デザイン・パターンは , スレッドにも対応しなければならないのです。 Singleton オプジェクトは , 共有される グローバル・リソースなのであり , 共有されるグローバル・リソースというものは全て , 競合条件とスレッドに 関連する問題の元凶となり得るのです。 6.9.1 Double-Checked Locking パターン マルチスレッド環境下における Singleton の包括的な考察は , Douglas Schmidt によって示されたのが最初で す ( 1996 ) 。同じ論文では , Doug Schmidt と Tim Harrison が発案した Double-Checked Locking パターンと いう , 非常に洒落た解決策が解説されています。 以下の解決策でも正しく動作するのですが , あまり面白いものではありません : Sing1eton& SingIeton: :lnstance()
6.10 全てを一つに によって決定されるわけです。スレッド・モデルがシングルスレッドのポリシーである場合 , 以下のような V01ati1eType がそのまま生成されます : template く class T> class Sing1eThreaded public : typedef T V01atiIeType ; 161 マルチスレッドのポリシーでは , T を volatile として限定します。スレッド・モデルについての詳細は , を参照して下さい。 では , 3 つのポリシーをつないだ工 nstance メンバ関数を定義してみましよう・ template く . T& SingIetonHOIder く . . > : :lnstance() if (destroyed-) if ()p 工 nstance—) typename ThreadingMode1 く T> : :LOCk guard; if ( !p 工 nstance—) return *plnstance_ LifetimePoIicy く T> : : Schedu1eCa11(&DestroySing1eton) ; plnstance— CreationP01icy く T> : : Create ( ) ; false; destroyed— LifetimeP01icy く T> : : OnDeadReference ( ) ; 付録 lnstance は , Sing1etonH01der が公開している唯一の public 関数です。 lnstance は , CreationP01icY' LifetimeP01icy, ThreadingM0de1 の器を実装したものです。ポリシー・クラス ThreadingM0de1 く T> は , 内 部クラス Lock を公開しています。 Lock オプジェクトの寿命があるうちは , Lock 型のオプジェクトを生成しよ うとする他のスレッドは全てプロックされます。 ( 付録を参照して下さい。 ) DestroySing1eton は , 単に Singlet0 Ⅱオプジェクトを破棄し , 割り当てられたメモリをクリーン・アップ し , destroyed- を true にします。 Sing1etonH01der が DestroySing1eton を呼び出すことはありません。 そのアドレスを LifetimeP01icy く T>: :Schedu1eDestruction へと引き渡すだけです : template く . void Sing1etonH01der く . assert ( ! destroyed—) ; CreationPoIicy く T> : :Destroy(pInstance—) ; . > : :DestroySing1eton()