シングルトンとオブジェクトの寿命

この記事は、最初の記事「シングルトンパターンの使用」[0]の続きです。 最初はこの記事でライフタイムに関連するすべてのものを設定したかったのですが、材料の量が多いことが判明したため、いくつかの部分に分けることにしました。 これは、さまざまなテンプレートとテクニックの使用に関する一連の記事の続きです。 この記事では、シングルトンの使用期間と開発に焦点を当てています。 2番目の記事を読む前に、最初の記事[0]を読むことを強くお勧めします。



前の記事では、シングルトンの次の実装が使用されました。

template<typename T> T& single() { static T t; return t; }
      
      







関数singleは、貴重なシングルトンを返しました。 ただし、このアプローチには欠点があります。この場合、オブジェクトのライフタイムを制御せず、このオブジェクトを使用したい時点で廃止される可能性があります。 したがって、new演算子を使用してオブジェクトを作成するには、別のメカニズムを使用する必要があります。



C ++にはガベージコレクターがないため、オブジェクトの作成と破棄を監視する必要がありました。 そして、この問題は長い間知られており、それを解決する方法でさえ明らかですが、この種のエラーは私たちのプログラムでは珍しいゲストではありません。 一般に、プログラマが犯す次のタイプのエラーを区別できます。

  1. オブジェクトが作成されないときのメモリ使用量。
  2. すでに削除されたオブジェクトのメモリ使用量。
  3. オブジェクトによって占有されている非解放メモリ。


そのようなエラーの結果として、プログラムは「リーク」を開始するか、予測不能な動作を開始するか、単に「フォール」します。 もちろん、さらに悪いことについて話すこともできますが、1つはっきりしていることは、そのようなエラーは非常に深刻なことです。



シングルトンの例を使用すると、このようなエラーがどのように発生するかを簡単に示すことができます。 ウィキペディアの記事[1]を開き、C ++の実装を見つけます。

 class OnlyOne { public: static OnlyOne* Instance() { if(theSingleInstance==0) theSingleInstance=new OnlyOne; return theSingleInstance; } private: static OnlyOne* theSingleInstance; OnlyOne(){}; }; OnlyOne* OnlyOne::theSingleInstance=0;
      
      





メモリがシングルトンに割り当てられていることがわかりますが、何らかの理由で解放されていません。 もちろん、その寿命はプログラムの時間と一致するため、シングルトンにとってこれは重大な問題ではないと言うことができます。 ただし、番号はありますが:

  1. メモリリーク検出プログラムは、シングルトーンのこれらのリークを常に表示します。
  2. シングルトンは、オープンな構成ファイル、データベースとの通信などを提供するかなり複雑なオブジェクトです。 このようなオブジェクトを誤って破棄すると、問題が発生する可能性があります。
  3. すべては小さなものから始まります。最初に、シングルトーンのメモリを監視せず、残りのオブジェクトを監視します。
  4. そして、主な質問:あなたがそれを正しくできるなら、なぜ間違っているのですか?


もちろん、これらの議論は無関係であると言うことができます。 ただし、本来の方法で実行してみましょう。 オブジェクトの操作には、常に次の原則を使用します。 作成されたオブジェクトは破棄する必要があります 。 そして、それがシングルトンであるかどうかは関係なく、例外のない一般的なルールであり、プログラムの特定の品質を設定します。



さまざまなソフトウェア製品のソースコードを自分で分析して、さらに2つの重要なルールを特定しました。

  1. newは使用しないでください。
  2. 削除を使用しないでください。


私が言っていることを少し説明する価値があります。 とにかくどこかnewとdeleteが呼ばれることは明らかです。 重要なのは、これは厳密には1つの場所にあるべきであり、1つのクラスではプログラムにスプレーしないようにするためです。 次に、このクラスを適切に編成すると、オブジェクトの存続期間を監視する必要がなくなります。 そして、私はすぐにこれが可能であると言います! 私がそのようなアプローチに出会ったことがないことをすぐに言及する価値があります。 だから私たちは一種の発見者になります。



スマートポインター



幸いなことに、C ++にはスマートポインターと呼ばれる優れたツールがあります。 それらの賢さは、通常のポインタのように動作しますが、オブジェクトの寿命も制御することです。 これを行うには、オブジェクトへのリンクの数を個別にカウントするカウンターを使用します。 カウンターがゼロに達すると、オブジェクトは自動的に破棄されます。 メモリヘッダーファイルの標準std :: shared_ptrライブラリのスマートポインターを使用します。 このようなクラスは、C ++ 0x標準をサポートする最新のコンパイラで使用できることに注意してください。 古いコンパイラを使用している場合は、boost :: shared_ptrを使用できます。 インターフェースはまったく同じです。



クラスAnに次の義務を割り当てます。

  1. スマートポインターを使用してオブジェクトの有効期間を制御します。
  2. 呼び出しコードで新しい演算子を使用せずに、派生クラスを含むインスタンスを作成します。




次の実装は、これらの条件を満たします。

 template<typename T> struct An { template<typename U> friend struct An; An() {} template<typename U> An(const An<U>& a) : data(a.data) {} template<typename U> An(An<U>&& a) : data(std::move(a.data)) {} T* operator->() { return get0(); } const T* operator->() const { return get0(); } bool isEmpty() const { return !data; } void clear() { data.reset(); } void init() { if (!data) reinit(); } void reinit() { anFill(*this); } T& create() { return create<T>(); } template<typename U> U& create() { U* u = new U; data.reset(u); return *u; } template<typename U> void produce(U&& u) { anProduce(*this, u); } template<typename U> void copy(const An<U>& a) { data.reset(new U(*a.data)); } private: T* get0() const { const_cast<An*>(this)->init(); return data.get(); } std::shared_ptr<T> data; };
      
      







提案されている実装についてさらに詳しく説明する価値があります。

  1. コンストラクターは、C ++ 0x標準の移動セマンティクス[6]を使用して、コピーのパフォーマンスを向上させます。
  2. createメソッドは、目的のクラスのオブジェクトを作成します。デフォルトでは、クラスTのオブジェクトが作成されます。
  3. produceメソッドは、受け入れられた値に応じてオブジェクトを作成します。 この方法の目的は後述します。
  4. copyメソッドは、クラスの詳細コピーを作成します。 コピーには、クラスの実際のインスタンスのタイプをパラメーターとして指定する必要があり、基本タイプは適切ではないことに注意してください。




この場合、シングルトンは次のように書き換えられます。

 template<typename T> struct AnAutoCreate : An<T> { AnAutoCreate() { create(); } }; template<typename T> T& single() { static T t; return t; } template<typename T> An<T> anSingle() { return single<AnAutoCreate<T>>(); }
      
      







ヘルパーマクロは次のようになります。

 #define PROTO_IFACE(D_iface, D_an) \ template<> void anFill<D_iface>(An<D_iface>& D_an) #define DECLARE_IMPL(D_iface) \ PROTO_IFACE(D_iface, a); #define BIND_TO_IMPL(D_iface, D_impl) \ PROTO_IFACE(D_iface, a) { a.create<D_impl>(); } #define BIND_TO_SELF(D_impl) \ BIND_TO_IMPL(D_impl, D_impl) #define BIND_TO_IMPL_SINGLE(D_iface, D_impl) \ PROTO_IFACE(D_iface, a) { a = anSingle<D_impl>(); } #define BIND_TO_SELF_SINGLE(D_impl) \ BIND_TO_IMPL_SINGLE(D_impl, D_impl) #define BIND_TO_IFACE(D_iface, D_ifaceFrom) \ PROTO_IFACE(D_iface, a) { anFill<D_ifaceFrom>(a); } #define BIND_TO_PROTOTYPE(D_iface, D_prototype) \ PROTO_IFACE(D_iface, a) { a.copy(anSingle<D_prototype>()); }
      
      







BIND_TO_IMPL_SINGLEマクロには小さな変更が加えられ、単一の関数ではなくanSingle関数が使用されるようになりました。この関数は、Anの既に満たされたインスタンスを返します。 他のマクロについては後で説明します。



シングルトンを使用する



次に、説明したクラスを使用してシングルトンを実装することを検討してください。

 // header file struct X { X() { x = 1; } int x; }; //    DECLARE_IMPL(X) // cpp file struct Y : X { Y() { x = 2; } int y; }; //   X   Y   BIND_TO_IMPL_SINGLE(X, Y)
      
      







現在では、次のように使用できます。

 An<X> x; std::cout << x->x << std::endl;
      
      







画面上のそれは、番号2を与えます。 実装には、クラスYが使用されました。



寿命管理



ここで、シングルトーンにスマートポインターを使用することの重要性を示す例を考えてみましょう。 これを行うには、次のコードを分析します。

 struct A { A() { std::cout << "A" << std::endl; a = 1; } ~A() { std::cout << "~A" << std::endl; a = -1; } int a; }; struct B { B() { std::cout << "B" << std::endl; } ~B() { std::cout << "~B" << std::endl; out(); } void out() { std::cout << single<A>().a << std::endl; } };
      
      







次に、このout関数の呼び出しで何が表示されるかを見てみましょう。

 single<B>().out(); //    B A 1 ~A ~B -1
      
      





ここで何が起こるか見てみましょう。 最初に、クラスBの実装をシングルトンから取得する必要があるため、クラスBを作成し、シングルトンからクラスAの実装を取得し、値aを取得する関数を呼び出します。 aの値はコンストラクタAに設定されているため、画面に数字1が表示され、プログラムは作業を終了します。 オブジェクトは逆の順序で破壊され始めます。 最初に、最後のクラスによって作成されたクラスAが破棄され、次にクラスBが破棄されます。クラスBが破棄されると、シンセトンから再び関数が呼び出されますが、 オブジェクトAはすでに破棄されており、画面に碑文-1が表示されます。 一般的に、プログラムは次のように崩壊する可能性があります すでに破壊されたオブジェクトのメモリを使用します。 したがって、この実装は、ライフタイムの制御がなければ、プログラムが最後に安全に落ちる可能性があることを示しています。



オブジェクトの存続期間を制御しながら、同じことを行う方法を見てみましょう。 これを行うには、クラスAnを使用します。

 struct A { A() { std::cout << "A" << std::endl; a = 1; } ~A() { std::cout << "~A" << std::endl; a = -1; } int a; }; //   A      BIND_TO_SELF_SINGLE(A) struct B { An<A> a; B() { std::cout << "B" << std::endl; } ~B() { std::cout << "~B" << std::endl; out(); } void out() { std::cout << a->a << std::endl; } }; //   B      BIND_TO_SELF_SINGLE(B) //  An<B> b; b->out();
      
      







このコードは、次の重要な詳細を除いて、実際には以前のものと変わりません:

  1. オブジェクトAとBは、シングルトーンにAnクラスを使用します。
  2. クラスBは、クラスの対応するパブリックメンバーを使用して、クラスAへの依存関係を明示的に宣言します(このアプローチの詳細については、前の記事を参照してください)。




画面に現在表示されているものを見てみましょう。

 B A 1 ~B 1 ~A
      
      





ご覧のとおり、クラスAの寿命を延長し、オブジェクトの破棄の順序を変更しました。 -1が存在しないということは、データにアクセスしている間にオブジェクトが存在したことを意味します。



合計



これで、オブジェクトの存続期間に関する記事の最初の部分は終わりました。 次の部分では、開発された機能を使用した残りの生成デザインパターンが分析され、一般的な結論が導き出されます。



PS



多くの人が尋ねます、そして実際、何がポイントですか? なぜあなたはシングルトンを作れないのですか? 明確さを増やさないが、コードを複雑にするだけの追加の構造を使用する理由。 原則として、 最初の記事[0]を注意深く読めば、このアプローチがより柔軟であり、多くのシングルトンの重大な欠点を排除することをすでに理解できます。 次の記事では、なぜこのように描いたのかが明確に理解されます。 シングルトンだけでなく、すでに話しているでしょう。 そして、この記事を通じて、シングルトンがそれとはまったく関係がないことは一般的に明らかになります。 私が見せようとしているのは、 依存関係の反転原理の使用[4]ですOODの原理[5]も参照)。 実際、Javaでこのアプローチを初めて見た後、これがC ++であまり使用されていないことに腹を立てました(原則として、同様の機能を提供するフレームワークがありますが、もっと軽量で実用的なものが欲しいです)。 上記の実装は、この方向へのほんの小さなステップであり、すでに大きなメリットがあります。



また、特定の実装を従来のシングルトンと区別するいくつかの点に注意したいと思います(一般的に、これらは結果ですが、重要です)。

  1. シングルトンクラスは、制限なしで複数のインスタンスで使用できます。
  2. シングルトンは、オブジェクトのインスタンス数を制御するanFill関数を使用して暗黙的にフラッディングされ、必要に応じてシングルトンの代わりに特定の実装を使用できます( 最初の記事[0]を参照)。
  3. クラスインターフェイス、実装、インターフェイスと実装の関係という明確な分離があります。 それぞれが彼のタスクのみを解決します。
  4. シングルトン依存関係の明示的な説明、この依存関係をクラスコントラクトに含める。




更新する



コメントを読んだ後、明確にする必要のあるポイントがあることに気付きました。 多くは、依存関係の逆転の原則、DIPまたは制御の逆転、IoCに精通していません。 次の例を考えてみましょう。たとえば、ユーザーのリストなど、必要な情報を含むデータベースがあります。

 struct IDatabase { virtual ~IDatabase() {} virtual void beginTransaction() = 0; virtual void commit() = 0; ... };
      
      





必要なユーザーを含む、必要な情報を提供するクラスがあります。

 struct UserManager { An<IDatabase> aDatabase; User getUser(int userId) { aDatabase->beginTransaction(); ... } };
      
      





ここでは、何らかのデータベースが必要であると言うDatabaseメンバーを作成します。 彼がどんな種類のデータベースになるかを知ることは重要ではなく、誰がいついつデータを入力するかを知る必要はありません。 しかし、UserManagerクラスは、必要なものがそこに注がれていることを知っています。 彼は次のように述べています。「必要な実装を教えてください。どの実装が必要かわかりません。このデータベースから必要なことはすべて行います。たとえば、このデータベースからユーザーに関する必要な情報を提供します。」



今、私たちはトリックを行います。 すべての情報を含むデータベースは1つしかないので、「データベースは1つしかないので、シングルトンを作成し、実装のたびにスチームバスを浴びないように、シングルトン自体を埋めます」と言います。 :

 struct MyDatabase : IDatabase { virtual void beginTransaction(); ... }; BIND_TO_IMPL_SINGLE(IDatabase, MyDatabase)
      
      





つまり MyDatabaseの実装を作成し、BIND_TO_IMPL_SINGLEマクロを使用してシングルトンに使用するとします。 次に、次のコードが自動的にMyDatabaseを使用します。

 UserManager manager; User user = manager.getUser(userId);
      
      





時間が経つにつれて、ユーザーがいる別のデータベースがあることが判明しましたが、たとえば別の組織用です。

 struct AnotherDatabase : IDatabase { ... };
      
      





もちろん、UserManagerを使用したいのですが、別のデータベースを使用します。 問題ありません:

 UserManager manager; manager.aDatabase = anSingle<AnotherDatabase>(); User user = manager.getUser(userId);
      
      





そして魔法のように、今度は別のデータベースからユーザーを取得します! これはかなり粗雑な例ですが、依存関係処理の原理を明確に示しています。これは、従来のアプローチではなく、IDatabase実装がUserManagerであふれ、UserManager自体が必要な実装を検索する場合です。 この記事では、この原則を使用しますが、実装のためのシングルトンは特別な場合と見なされます。



文学



[0] シングルトンパターンの使用

[1] ウィキペディア:シングルトン

[2] C ++内部:シングルトン

[3] パターンの生成:シングルトン

[4] 依存関係反転の原理

[5] OODの原則

[6] ウィキペディア:右辺値参照と移動セマンティクス



All Articles