ホーム < ゲームつくろー! < デザインパターン習得編

Singleton
  〜存在するオブジェクトは1つだけ


@ 唯一厳格に1つということ


 Singletonパターンは、アプリケーション内でオブジェクトが1つしか無い事を保障する生成法です。オブジェクトが複数あると困る事はたびたびあります。例えば、ゲーム内で通して使われるグローバルなタイマー。これが複数あると、ゲームに2つの時間軸が存在してしまうことになります。時計は1つで良いのです。

 「グローバルな変数を1つだけ用意すれば?」と考えがちですが、それは作り手側の配慮です。2つ用意することも出来てしまいます。Singletonパターンは「2つ用意するとエラーになる」くらいの厳格なものなのです。

 グローバルなタイマーをSingletonパターンで生成してみます。

class GlobalTimer
{
public:
   static GlobalTimer* Instance();
   int GetTime();

protected:
   GlobalTimer();

private:
   static GlobalTimer* _Instance;
}

 このクラスは、通常のクラスと相当に変わっているところがあります。まず、コンストラクタがprotected宣言されています。これは、GlobalTimerのお友達(派生クラスなど)でないとこのクラスのオブジェクトを生成できないことを示しています。main関数で、

 GlobaleTimer GT;

とするとコンパイルエラーとなってしまいます。「じゃぁ、どうやってオブジェクトを作るの?」と疑問に思いますよね。そのために、Instance関数がstaticでpublicになっており、また_Instanceがstaticでprivateになっているのです。

 クラス内でstatic宣言された変数は、実行と同時にその格納場所(メモリ)が唯一1つ決定してしまいます。実行と同時に実体を持つわけです。そして、このクラスのオブジェクトは同じ値を共有します。上のクラスでGlobalTimer型のポインタである_Instanceは、唯一ひとつなわけです。次に、static関数は、実行と同時に関数ポインタが固定されます。Instance関数もまた、唯一ひとつです。ということは、Instance関数を通して_Instanceを取り出せば、唯一ひとつのオブジェクトにいつもアクセスすることになります。

 肝心のオブジェクト生成方法は、次の手順を踏みます。まず、グローバル変数として、次のように宣言します。

GlobalTimer* GlobalTimer::_Instance = 0;

「なんじゃこれは!」ですよね。GlobalTimer::_Instanceはクラス内でprivateでstaticに宣言された変数です。それに0を代入している。「privateな変数にアクセスしているじゃないか!」「なんで0を代入するの?」・・・面白いんです、これが。

 まず、クラス内で宣言されたstatic変数はグローバル変数として「初期化」することが許されています。これはprivateだろうとprotectedだろうと関係ありません。「初期化はコンストラクタだろう!」と怒られそうですが、先に述べたように、GlobalTimerクラスはコンストラクタがprotectedで宣言されているので、実体を定義できないのです。つまり、コンストラクタが呼ばれないので、初期化が出来ない。そのため、上のような初期化をするわけです。

 0を代入しているのはなぜでしょうか?1とか100とかではだめなのでしょうか?試しに1を代入すると、ここはコンパイルエラーになってしまいます。「const int型をGlobalTimer*型に変換できません」と言われるんです。これは暗黙の型変換が出来ないんですね。しかし0はこのエラーが出ません。ここの0というのは「NULL」と同意なんです。NULLはどんなポインタにも代入が可能ですよね。よって、この初期化は「0(=NULL)」以外にはできません。

 グローバル領域にて_Insntanceの初期化が済んだら、次に、Instance関数を呼び出します。

GlobalTimer* GT = GlobalTimer::_Instance();

「あれ?GlobalTimerは定義できないんじゃ・・・?」。確かに実体は定義できませんが「ポインタ変数」は定義できます。これは、intとint*がまったく別の変数であることを考えれば、至極当然です。もちろん、コンストラクタは呼ばれません。GlobalTimer::Instance関数はstaticでしかもpublicなので、いつでもアクセスが可能です。そして、この関数はGlobalTimer::_Instanceを返します。

GlobalTimer* GlobalTimer::Instance()
{
   if(_Instance == 0)
      _Instance = new GlobalTimer;

   return _Instance;
}

 _Instanceが0の時、オブジェクトを生成しています。それ以外の時には、ただオブジェクトへのポインタを返すだけです。_Instanceへアクセスする方法が唯一Instance関数だけで、共にstatic宣言されているために、アプリケーションに1つであることが保障されます。また、NULLポインタにもならないので、安全です。

 Singletonパターンの関係図は次のようになります。

 GlobalTimerに置き換えてみると、

 となります。


A Singletonオブジェクトの生成タイミングに注意

 パズルのような実装で「唯一性」を保つSingletonパターンですが、注意することがあります。それは、グローバル変数は生成される順序がわからないという点です。2つのSingletonオブジェクトがお互いに関連しあう場合、生成順序によってはエラーが発生する可能性があります。

class FirstSingleton
{
private:
   static FirstSingleton* m_FS;
   static int m_First;

public:
   static int GetVal(){return m_First;}
   static FirstSingleton* Instance();
class SecondSingleton
{
private:
   static SecondSingleton* m_pSS;
   static int m_Second;

public:
   static SecondSingleton* Instance();
};

 2つのクラスがあります。SecondSingletonクラスのオブジェクトを生成するときに、FirstSingletonのGetVal関数を呼び出して、FirstSingleton::m_Firstと同じ値にしておくという「初期化」をするとします。

int SecondSingleton::m_Second = FirstSingleton::GetVal();

ところが、グローバル変数は生成される順序がわからないので、FirstSingletonがまだ初期化される前にこの代入が実行されてしまう事があります。すると、m_Secondには不定の値が入ってしまい、わかりにくいバグが生じます。Singletonパターンを使用するときには、他のSingletonに依存する実装にしてはいけません。