Sing - みる会図書館


検索対象: Modern C++ Design
34件見つかりました。

1. Modern C++ Design

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 による登録が発生した場合の定義が行われていないのです。 この問題を明確にするため , 小さなテスト・プログラムを考察してみましよう :

2. Modern C++ Design

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 生成を抑止する

3. Modern C++ Design

164 class A { 第 6 章 Singleton の実装 typedef Sing1etonH01der く A , CreateUsingNew> Sing1eA; / / こ以降 Sing1eA: : 工 nstance() が使用可能になる。 導出したクラスのオプジェクトを返す S ⅲ gleton を提供するのも , creator ポリシー・クラスの変更と同じく らい簡単です : class A { class Derived : public A { template く class T> struct MyCreator static T* Create() return new Derived; public CreateUsingNew く T> typedef Sing1etonH01der く A, StaticA110cator , MyCreator> Sing1eA; ファイル内にて行うべきものです : ンプレートを使用して解決した KDL 問題です。こういった定義は , もちろんのことながら , 適切なヘッダ・ KDL は込み入った問題であるため , いったん仮の実装を行ってみましよう。以下が , Sing1eton クラス・テ これは , Sing1eA の型定義中で Sing1etonWithLongevity を用いる場合にのみ必要となります。 inline unsigned int GetLongevity(A*) { return 5 ; } ることを前提としています。この定義は以下のようなものになります : Sing1etonWithLongevity ポリシー・クラスは , ネームスペース・レベルで関数 GetLongevity カゞ定義され 受けながら , 必要に応じて Si Ⅱ glet 。Ⅱを大きくカスタマイズすることができるわけです。 す。各ポリシーに従って Sing1et 。 n を微調整することもできます。このように , デフォルト動作による恩恵を 同じようにして , コンストラクタへのパラメータを提供したり , 異なった割り当て戦略を用いることもできま class LogImp1 { class Disp1ayImp1 { class KeyboardImp1 { こういったものによって , 問題の複雑さが容易に把握でき , 自己記述性のある解決が行えるようになるわけです。 typedef Sing1etonH01der く LogImp1 , Sing1etonWithLongevity> Log; typedef Sing1etonHoIder く DispIayImpI , SingIetonWithLongevity> Disp1ay; typedef Sing1etonH01der く KeyboardImpI , SingIetonWithLongevity> Keyboard; inline unsigned int GetLongevity(LogImpI*) { return 2 ; } / / ログはより長い寿命である inline unsigned int GetLongevity(Disp1ayImpI*) { return 1 ; } inline unsigned int GetLongevity(KeyboardImp1*) { return 1 ; }

4. Modern C++ Design

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 のデストラクタが呼び出されます。

5. Modern C++ Design

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 を参照して下さい。 )

6. Modern C++ Design

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-

7. Modern C++ Design

160 ルのロックではなくクラス・レベルのロックのみををサポートしています。これは , 1 つしか存在しないためです。 6.10.3 Sing1etonH01der の組み立て 第 6 章 Singleton の実装 該当オプジェクトが絶対に では , Sing1etonH01der クラス・テンプレートの定義を始めましよう。第 1 章で考察したように , 各ポリ シーは独立した 1 つのテンプレート・パラメータになります。それに加えて , Singleton としての動作を提供 する型をテンプレート・パラメータ (T) として指定します。 Si Ⅱ glet 。Ⅱ H 。 lder クラス・テンプレート自身は , Sing1eton ではありません。 Sing1etonH01der は , 既存クラスに対して Singleton の動作とその管理を提供す るだけのものです : template class T, template く class> class CreationP01icy template く class> class LifetimePoIicy template く class> class ThreadingMode1 class Sing1etonH01der public : static T& lnstance() ; private : / / ヘルバー static void DestroySing1eton() ; / / 保護 Sing1etonH01der() ; / / データ CreateUsingNew , Defau1tLifetime , Sing1eThreaded typedef ThreadingM0de1 く T> : :V01atiIeType 工 nstanceType ; static InstanceType* plnstance— static b001 destroyed- インスタンス変数の型は T* ではなく ThreadingM0de1 く T> : : VoIati1eType* です ThreadingModeI く T>: :V01ati1eType 型の定義はスレッド・モデルに応じて展開され , T や volatile T となります。型に volatile 修飾子を適用することにより , その型の値がマルチスレッド環境下で変更される 可能性がある事をコンパイラに知らせることになります。こういった情報によって , コンパイラはある種の最適 化 ( 値を内部レジスタに保存するといった ) を抑止し , マルチスレッド時に誤った処理を行うコードの生成を避 けるわけです。つまり , plnstance- を volatile T * 型として定義するのが安全なのです。これによって , マル チスレッドのコードでも実行でき ( お使いのコンパイラのドキュメントをチェックする必要はあります ) , シン グルスレッドのコードにも影響を与えないわけです。 一方 , シングルスレッド・モデルでは , 最適化のメリットを最大限に活かしたいでしようから , p 工 nstance- を T * 型として定義するのが最適なのです。つまり , p 工 nstance ーの実際の型は , スレッド・モデルのポリシー

8. Modern C++ Design

162 plnstance— = destroyed— = true ; 第 6 章 SingIeton の実装 Sing1etonH01der は , plnstance- と DestroySing1eton のアドレスを LifetimeP01icy く T> に引き渡しま す。その目的は , 0 + の規則 , 再生成 (Phoenix SingIeton) , ユーザ制御 ( 寿命を指定する Singleton) , 無限と いった動作を実装できる十分な情報を , LifetimeP01icy に引き渡すためです。以下がそれぞれの方法です : ・ C + + の規則によるもの : LifetimeP01icy く T> : : Schedu1eDestruction は , DestroySingIeton のアド レスを引き渡して atexit を呼び出します。 OnDeadReference は , 例外 std: :logic-error をスローし ・再生成によるもの : OnDeadReference が例外をスローしないことを除けば上記と同じです。 Sing1et 。Ⅱ H 。 lder の制御フローはそのまま続行され , オプジェクトが再度生成されます。 ・ユーザ制御によるもの : LifetimeP01icy く T> : : ScheduIeDestruction は , SetLongevity (GetLongevity(pInstance)) を呼び出します。 ・無限によるもの : LifetimeP01icy く T>: :ScheduIeDestruction の実装は空となっています。 Sing1etonH01der は , 死んだ参照に対する問題の責任を LifetimeP01icy に任せています。これ によってとても簡潔になるのです。 Sing1etonH01der : : 工 nstance が死んだ参照を検出した場合 , LifetimeP01icy: :OnDeadReference を呼び出すわけです。そして , OnDeadReference から制御が帰って きた場合 , lnstance は新たなインスタンスを再度生成することになります。つまり , Phoenix Singleton にし たくない場合 , 0nDeadRefere Ⅱ ce は例外をスローするかプログラムを終了させることになります。 Phoenix Singleton の場合 , OnDeadReference は何も行いません。 これが Si Ⅱ glet 。 nH01der の実装全てです。様々な作業が 3 つのポリシーに委譲されたわけです。 6.10.4 ポリシーの実装 ポリシーへの分解は難しいものですが , できてしまえば , ポリシーの実装は簡単になります。では , Singleton の共通部分を実装するポリシー・クラスを整理してみましよう。表 6.1 は , Sing1etonH01der における定義済 みポリシー・クラスの一覧です。下線が引かれたポリシー・クラスは , デフォルトのテンプレート・パラメータ です。 残っているのは , この小さいながらも万能の Si Ⅱ glet 。 nH 。 lder テンプレートを使用する方法と , 拡張する方 法だけです。 6.11 Sing1etonH01der を用いる Sing1etonH01der クラス・テンプレートは , アプリケーションに特化した機能を提供しません。単に , この 章におけるコード中の T という他のクラスに対して , SingIet 。 n に特化したサービスを提供しているのです。 の T をクライアント・クラスと呼びます。 クライアント・クラスは , 自動的に構築や破棄が行われないよう , 全ての予防策を講じておかなければなりま せん。つまり , デフォルト・コンストラクタ , コピー・コンストラクタ , 代入演算子 , デストラクタ , アドレス

9. Modern C++ Design

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 死んだ参照の問題 における代替実装を考えてみましよう。 います。これはほとんどの場合でうまく動作します。では , その問題を考察し , いくつかの改善と特殊なケース この章の残りを通じて用いる例を決めておきましょ

10. Modern C++ Design

6.11 Sing1etonH01der を用いる 163 表 6.1 Sing1etonH01der に対する定義済みポリシー 定義済みクラス・テンプレートコメント ポリシー Creation Lifetime ThreadingM0de1 CreateUsingNew CreateUsingMa110c CreateStatic Defau1tLifetime PhoenixSing1eton Sing1etonWithLongevity NoDestroy Sing1eThreaded C1assLeve1Lockab1e new 演算子とデフォルト・コンスト ラクタを用いてオプジェクトを生成 します。 std : : malloc とデフォルト・コン ストラクタを用いてオプジェクトを 生成します。 静的記憶域中にオプジェクトを生成 します。 C + + の規則に従ってオプジェクトの 寿命を管理する。作業を完了させる ために atexit を使用します。 Defau1tLifetime と同じですが Singleton オプジェクトの再生成が できます。 Singleton オプジェクトに寿命を設 定します。 plnstance- を受け取り , Singleton オプジェクトの寿命を返 す , ネームスペース・レベルの関数 GetLongevity が存在するものと仮 定します。 Singleton オプジェクトを破棄しま せん。 スレッド・モデルについての詳細は 付録を参照して下さい。 演算子は private とする必要があるわけです。 引き渡すのです : を呼び出す際にフラグやオプションを引き渡すように , 必要な動作を選択するために型の定義に対してフラグを 特定の Singlet 。Ⅱ実装に関する設計上の決定は , 通常の場合 , 以下のような方法で型定義に反映します。関数 良いという点に注意して下さい。 る不都合さ , および不適切な ( 十分使える範囲で ) インスタンスとなるというリスクを天秤に掛けて判断しても をクラスに対して行うことになります。ただし , こういった変更はオプショナルであり , 既存のコードを修正す 要があります。つまり , Sing1etonH01der を用いて作業を行う場合 , 上記の予防策と f て iend 宣言という変更 こういった予防策を講じた上で , 使用する creator ポリシー・クラスとの友好関係も同時に設定しておく必