13.x Singleton
この項ではSingletonと言われるデザインパターンについて説明します。当知見を得る事で、唯一性が保証される型を作る事ができるようになります。
13.x.1 Singletonの概要
Singletonデザインパターンとは、そのクラスからインスタンス化されるオブジェクトが同一時間上の全ての翻訳単位において唯一である事を保証するデザインパターンです。 Singletonデザインパターンが取り上げられるのは、C++言語だけでなく、様々な実現方法が考案されていますが、主にC++でのSingletonデザインは二種類に分類する事ができます。 まずは、二つのSingletonデザインパターンのコードを見ていただきましょう。
struct single{
single(const single&)=delete;
single&& operator=(const single&)=delete;
static single& get()
{
return *data_;
}
static void instance()
{
if(!data_)data_=new single();
}
void destroy()noexcept
{
if(data_){
delete data_;
data_=nullptr;
}
}
private:
single()=default;
~single()=default;
static single* data_;
};
single* single::data_=nullptr;
まずコピーができてしまっては、オブジェクトの唯一性が保てませんので、コピーコンストラクタとコピー代入演算子が削除されています。ムーブコンストラクタとムーブ代入演算子も外部からのコンストラクタを禁止するため削除しなければなりませんが、コピーコンストラクタとコピー代入演算子が削除済みなので、ムーブコンストラクタとムーブ代入演算子はそれと同時に暗黙的に削除されるため、明示する必要はありません。
次に、コンストラクタとデストラクタがprivate
アクセスレベル空間に宣言されていますね。これも唯一性を保つためです。二度以上外部からコンストラクタを起動してしまえば唯一性が保てなくなります。デストラクタは、勝手に外部から破棄されると困るのでprivate
に指定しています。
そして、instance
というstatic
なメンバ関数は、single
クラスに対する唯一のインスタンス化方法です。内部では、自らの型のポインタに対するヌルチェックが行われており、一度オブジェクトがインスタンス化されていた場合は破棄(destroy
関数が呼ばれるまで)されていない限りは新たにnew
が実行されないようになっています。
生成されたオブジェクトに対するアクセスはget
関数から行います。この場合、勿論コピーは禁止ですので、lvalue referenceで受け取る必要があります。
destroy
は、生成したオブジェクトを破棄する専用の関数です。簡単なヌルチェックを行い、オブジェクトをdelete
します。この後、nullptr
を入れておく事で次回にinstance
関数が呼ばれた場合に、新たにオブジェクトを生成する事が保証されます。
これは以下のように使います。
int main()
{
single::instance();
single& s=single::get();
s.destroy();
}
このような動的な領域確保によるSingletonパターンのメリットとして挙げられるのは、オブジェクトの生成、破棄のタイミングを完全にコントロールできる点です。もし例えばこのクラスをインクルードしたとしても、instance
関数によってインスタンス化しない限り、無駄にオブジェクトが生成される事は全くありません。また、破棄のタイミングをコントロールできるという事は、他のオブジェクトよりも長生きして欲しいといった場合にその点で融通が利きます。
逆にデメリットとして考えられるものは、まず解放忘れに関してでしょう。オブジェクトの寿命のコントロールに融通が効くという事は、逆にオブジェクトの管理責任が全てプログラマ側に回るという事です。プログラムが終了するまでに、destroy
関数を必ず呼び出さなければなりません。何も手作業でdestroy
を呼び出さなくとも、例えばスコープ単位の破棄処理などと組み合わせて使えば良いと思うかもしれませんが、事態が単純ならまだ良いものの、マルチスレッドなどで複雑に入り組んでいる場合は、その全パターンで正常にdestroy
を呼び出すようにチェックするコストは、やはり負わなければならなりません。
また、上記のようにヒープから動的に領域を確保する事は、静的領域やスタックから領域を確保するよりもパフォーマンスに劣るという事も考えられます。
次に、もう一つの主なSingletonパターンの実装も見て見ましょう。
struct single{
single(const single&)=delete;
single& operator=(const single&)=delete;
static single& instance()noexcept
{
static single inst;
return inst;
}
private:
single()=default;
~single()=default;
};
これは、以下のように使います。
single& s=single::instance();
まず、前述の実装と同じように、コピーとコピー代入を禁止し、コンストラクタとデストラクタをprivate
アクセスレベルに設定します。そして、唯一のインスタンス、アクセス手段がinstance
関数に集約されています。
内部ではstatic
なsingle
クラスのオブジェクトが生成され、そのlvalue referenceを返しています。static
な変数は、既にインスタンス化されていた場合、新たにオブジェクトを生成する事はないので、唯一性がこの時点で保証されるのです。
この実装のメリットを考えて見ましょう。パフォーマンスの面からいうと、前述したSingletonの実装よりも高速なコードが生成される事が伺えます。動的な領域確保はしていませんし、if
文による分岐がありません。前述のinstance
関数と同じく同関数を実行するまでは余計なオブジェクトがインスタンス化されません。
また、Singletonのリソースは静的領域を用いているため、プログラマ側が解放処理を行わなくとも良い事となります。
逆にデメリットは、リソースの管理をコントロールできないという点です。例えば、Singletonのオブジェクトに最も長生きして欲しければ、全てのオブジェクトよりも最も早くインスタンス化しなければならないのです(オブジェクトの破棄は最初に生成されたオブジェクトを最後に破棄するLIFO形式で行われるため)。このような制限はとても良くはありません。
このような問題に対する解決策も講じられています。 例えばPhoenix Singletonパターンなどがそうです。Phoenix Singletonパターンは、破棄済みかどうかのフラグを静的領域に持たせて、破棄済みであった場合は配置newによって再度オブジェクトを生み出すといったような実装です。確かに、使おうと思った時にそのオブジェクトが破棄されていなければ何の問題もないので、これによって、インスタンス化されるタイミングによって定められる破棄の順序とは異なる挙動をしているように見せる事ができます。 しかし、Phoenix Singletonも、場合によってはうまく動作しないか、少し手間のかかる処理をしなければならないかもしれません。 例えば、Phoenix Singletonが何か状態を内部に持つクラスであった場合、一度破棄されてしまったらその後再度生成された頃には内部に持っていた状態は消えてしまいます。また、LIFOに則ったオブジェクトとは異なる動きをする事は利点でもあると同時に、混乱の元となる可能性もあるのです。 もはやこうなると、寿命を完全にコントロールできる、動的に領域を確保する実装のSingletonパターンを使うか、別途寿命をコントロールする機構を実装しなければなりません。