2011/08/06

シングルトンクラスのススメ

シングルトンクラスって知ってますか?
そんなの当たり前じゃ~って方もたくさんいるとは思いますが、今回はシングルトンクラスについてつらつらと書きたいと思います。


シングルトンクラスというのは、「構造上インスタンスの存在を一つしか許さないクラス」です。なんだか難しいですが、元々の存在意味としては一つあれば十分で2つ以上あるとバグが起きたり遅くなったりするから、それ以上作られないようにコンストラクタをプレイベードで隠しちゃったクラスです。


今回はそんなシングルトンクラスの違った使い方を紹介したいと思います。厳密なオブジェクト指向からは離れるので、オブジェクト指向を厳密に守る人からは怒られますが、気軽にプログラムを書いてる時には意外と使えると思います。




ところで皆さんグローバル変数って使ってますか?




どこからでもアクセスできて色々できちゃうグローバル変数は設計をアレコレ考えなくても済むので楽ですが、一方で何かバグが起きたときに対処しにくいといった問題があります。
例えば、本当はdeleteしちゃいけないタイミングでdeleteした結果が、全く違う場所に影響を及ぼしちゃったり、入れちゃいけない数値が入ってしまった結果が全く違う場所で現れたりする場合です。こうなったらもう大変。ブレークポイントを作ってアレコレしてもバグの原因を突き止めるのは至難の技です。


こういう時にシングルトンクラスを使ってみようというのが今回の話題です。シングルトンクラスは実体が一つしかないのでグローバル変数としても使えるんですね。(もちろんこういう使い方は本来のオブジェクト指向から見ればNGでグローバル変数なんて使うなって話ですが)

ということでシングルトンクラスのサンプルプログラムを書いてみました。

/*
シングルトンクラスサンプルプログラム
*/
#include<assert.h>
#include<stdio.h>

//シングルトンクラスの定義

class Singleton{
private:
 //コンストラクタ・デストラクタをプライベートに入れて外部からの生成を阻止
 Singleton(){
  data = 0;
 }
 ~Singleton(){};
 //コピーコンストラクタ等も入れる。実体はない。
 Singleton(const Singleton &obj);
 Singleton& operator=(const Singleton& r);

 //ただ一つのインスタンスを定義
 static Singleton* instance;

 //内部データ
 int data;
public:
 //利用時に外部から初期化するための関数。
 //すでに実体がある場合
 //デバッグ時:assertによりエラーが吐き出されるようにする。
 //リリース時:すでにある実体が返される。
 
 static Singleton* init(){
  assert(instance==0);
  if(instance==0)
   return(instance = new Singleton());
  else return instance;
 }
 //外部からインスタンスを呼ぶための関数。
 static Singleton* getInstance(){
  return instance;
 }
 //終了時にインスタンスをデリートするための関数。安全のため0を入れておく。
 static void destroy(){
  delete instance;
  instance = 0;
 }
 //データのセット関数
 void setData(int a_data){
  assert(a_data<100);
  if(a_data<100)data = a_data;
 }
 //データ取得関数
 int  getData(){
  return data;
 }
};

//シングルトンクラスの実体。通常はクラス専用のソースコードに書いて外部のソースコードから見えなくする。
Singleton* Singleton::instance = 0;

int main(){
 Singleton::init()->setData(10);//シングルトンクラスの初期化

 printf("%d\n",Singleton::getInstance()->getData());

 Singleton* nisemono = new SIngleton();//コンストラクタがprivateに入っているのでコンパイルエラー
 Singleton::init();//二重初期化でエラー
 Singleton::getInstance()->setData(100);//範囲外のデータ設定でエラー

 getchar();

 Singleton::destroy();

 return 0;
}
それぞれ説明して行きたいと思います。

・コンストラクタetcをprivateに
private:
 //コンストラクタ・デストラクタをプライベートに入れて外部からの生成を阻止
 Singleton(){
  data = 0;
 }
 ~Singleton(){};
 //コピーコンストラクタ等も入れる。実体はない。
 Singleton(const Singleton &obj);
 Singleton& operator=(const Singleton& r);
まず、コンストラクタ、デストラクタ等を全てプライベードに入れます。これによって外部からのnew,deleteを防ぎます。もちろんコピーコンストラクタ等も実体が2つ以上になる原因になりますのでprivateに入れます。

しかし、これでは実体が持てないので次のようにします。

・シングルトンクラスの初期化・アクセス
static Singleton* instance;
public: 
//初期化関数
 static Singleton* init(){
  assert(instance==0);
  if(instance==0)
   return(instance = new Singleton());
  else return instance;
 }
};
Singleton* Singleton::instance = 0;
static関数でただ一つのインスタンスstatic Singleton* instance;を初期化するのです。これによって実体が一つできます。さらに二度目にinitが呼ばれた場合にassertでエラーを返すようにしてみました。これでデバッグがやりやすくなります。

あとはgetInstance()関数によって一つあるインスタンスを呼び出します。setData等データを変更する際に例外処理を設けてやれば間違った値を入れた瞬間にデバッガがエラーを吐いてくれます。(グローバル変数だとこうはいかない・・・)
そうでなくても、setData内にブレークポイントを設置したり、setする度にその数値を記録したりすることもできます。値変更を関数にできるだけでたくさんメリットがありますね。


・エラーを吐いてくれる例
最後にmain内にバグがでる例を載せておきました。
Singleton* nisemono = new SIngleton();//コンストラクタがprivateに入っているのでコンパイルエラー
Singleton::init();//二重初期化でエラー
Singleton::getInstance()->setData(100);//範囲外のデータ設定でエラー
こんな感じで勝手にエラーにしてくれると「原因不明のバグ」が生じる可能性がぐんと下がると思います。ついついグローバル変数を使ってしまう方にオススメです。

0 件のコメント:

コメントを投稿