[ C++で開発 ]

Singletonパターン

 C++でのSingletonパターンの実装は、一見簡単に見えますが、メモリの解放タイミング、マルチスレッド安全性、インスタンス数のコントロールと奥深い課題を抱えています。

GoF本のSingleton実装

書籍「オブジェクト指向における再利用のためのデザインパターン」(通称GoF本)より

目的

 あるクラスに対してインスタンスが1つしか存在しないことを保証し、それにアクセスするためのグローバルな方法を提供する。

結果

  1. インスタンスへのアクセスを制御する。
  2. 名前空間を減らす。
  3. オペレーションや内部表現を詳細化できる。(Sngletonクラスのサブクラス化)
  4. インスタンスの数を変えることができる。
  5. クラスオペレーションよりも柔軟である。

実装上の問題

  1. インスタンスの唯一性を保証する
  2. Singletonのサブクラス化

実装例

Singleton.h
class Singleton {
public:
    static Singleton* Instance();
protected:
    Singleton();
private:
    static Singleton* _instance;
};
Singleton.cpp
Singleton* Singleton::_instance = 0;

Singleton* Singleton::Instance() {
    if (_instance == 0) {
        _instance = new Singleton;
    }
    return _instance;
}

GoF本の実装の問題

GoF本の実装例のコードが抱える問題点を列挙します。

  1. コピーコンストラクタ、コピー代入演算子によってインスタンスの唯一性の保証が崩れる
    Singleton* s1 = Singleton::getInstace();
    Singleton* s2 = new Singleton(*s1);

    Singleton s3 = *s2;
    とすれば、新しいSingletonクラスのインスタンスが生成されます。
  2. インスタンスをdeleteされてしまうと、以後ダングリング参照問題が発生する
    Singleton* s1 = Singleton::getInstance();
    delete s1;
    Singleton* s2 = Singleton::getInstance(); //ダングリング参照
  3. マルチスレッド安全性がない
    if (instance == 0) {     // (1)
      instance = new Singleton; // (2)
    }

    スレッドT1で(1)を実行後(2)を実行する前にスレッドT2が(1)を実行すると、まだinstance==0が成立するため、2回Singletonがnewされてしまう。
  4. いつ誰がnewしたSingletonをdeleteするのか(最後までdeleteされない)

パターンハッチングでの実装

書籍「パターンハッチング」(John VLISSIDES著)より

GoFの著者陣の1人、John VLISSIDESが書いた本に、Singletonの実装が掲載されています。プログラム終了時に削除するSingletonDestroyerを導入しています。

Singleton.h
class Singleton {
public:
    static Singleton* instance();
protected:
    Singleton();
    Singleton(const Singleton&);
    friend class SingletonDestroyer;
    virtual ~Singleton() {}
private:
    static Singleton* _instance;
    static SingletonDestroyer _destroyer;
};

class SingletonDestroyer {
public:
    SingletonDestroyer(Singleton* = 0);
    ~SingletonDestroyer();
    void setSingleton(Singleton* s);
    Singleton* getSingleton();
private:
    Singleton* _singleton;
};
// Singletonの実装
Singleton* Singleton::_instance = 0;
SingletonDestroyer Singleton::_destroyer;

Singleton* Singleton::instance() {
    if (!_instance) {
        _instance = new Singleton;
        _destroyer.setSingleton(_instance);
    }
    return _instance;
}

// SingletonDestroyerの実装
SingletonDestroyer::SingletonDestroyer(Singleton* s) {
    _singleton = s;
}
SingletonDestroyer::~SingletonDestroyer() {
    delete _singleton;
}
void SingletonDestroyer::setSingleton(Singleton* s) {
    _singleton = s;
}
Singleton* SingletonDestroyer::getSingleton() {
    return _singleton;
}
Singleton.cpp

C++って、ヘッダーに宣言したメンバ関数を実装していなくてもエラーにならないんですね。ただし、実装がない関数を呼んでしまうと、リンクエラーとなります。よくよくC言語で考えれば納得です。

GoF本の実装での問題点については以下のとおりです。

  1. コピーコンストラクタがprotected化されているので、インスタンスの複製ができないようになっています。
  2. デストラクタがprotected化されているので、インスタンスのdeleteができないようになっています。ただし、friend宣言されたSingletonDestroyerからはdelete可能です。
  3. マルチスレッド安全性はありません。
  4. プロセス終了時に、Singletonクラスのstaticメンバ変数(実体)であるSingletonDestroyerインスタンスが削除され、その際SingletonDestroyerのデストラクタがSingletonクラスの唯一のインスタンスをdeleteしています。

応用

書籍では、さらにSingletonDestroyerをテンプレート化していろいろなSingletonパターン適用クラスに汎用的に対応するサンプルを紹介しています。

また、SingletonDestroyerではなく、C言語標準関数atexit()を使ったクリーンアップについて言及しています。

Effective C++/More Effecetive C++での実装

staticなメンバー変数ではなく、関数のstaticローカル変数で唯一のインスタンスを定義する方法を使用します。

本記事はSingletonパターンの実装を比較して検討することが目的のため、書籍中のコードとクラス名等は変えています。

Singleton.h
class Singleton {
public:
    static Singleton& instance();
private:
    Singleton();
    Singleton(const Singleton&);
};
Singleton.cpp
Singleton& Singleton::instance() {
    static Singleton instance;
    return instance;
}

GoF本の実装での問題点については以下のとおりです。

  1. コピーコンストラクタがprivate化されているので、インスタンスの複製ができないようになっています。
  2. ポインタではなく参照を返すので、deleteは不要です。
  3. マルチスレッド安全性はありません。
  4. プロセス終了時に、破棄されます。

また、GoF本の実装にはない問題点として、サブクラス化が困難ということがあります。

Doug Schmidt氏の実装(ACEでの実装)

マルチスレッド安全性を持たせるために、ダブルチェックロッキングを導入しています。また、削除については、ACE_Cleanupを継承して実現しています(コードは省略)。

Singleton.h
template <class TYPE, class ACE_LOCK>
class ACE_Singleton : public ACE_Cleanup {
public:
    static TYPE* instance();
    virtual void cleanup(void* param = 0);
protected:
    ACE_Singleton();
    TYPE instance_;
    static ACE_Singleton<TYPE, ACE_LOCK> *singleton_;
    static ACE_Singleton<TYPE, ACE_LOCK> *&instance_i();
};
Singleton.cpp
template <class TYPE, class ACE_LOCK>
TYPE* Singleton<TYPE, ACE_LOCK>::instance() {
    ACE_Singleton<TYPE, ACE_LOCK> *&singleton =
        ACE_Singleton<TYPE, ACE_LOCK>::instance_i();

    if (singleton == 0) {
        if (ACE_Object_Manager::starting_up() ||
            ACE_Object_Manager::shutting_down())
        {
            ACE_NEW_RETURN(singleton, (ACE_Singleton<TYPE, ACE_LOCK>), 0);
        } else {
            static ACE_LOCK *lock = 0;
            if (ACE_Object_Manager::get_singleton_lock(lock) != 0)
                return 0;
            ACE_GUARD_RETURN(ACE_LOCK, ace_mon, *lock, 0);
            if (singleton == 0) {
                ACE_NEW_RETURN(singleton, (ACE_Singleton<TYPE, ACE_LOCK>), 0);
                ACE_Object_Manager::at_exit(singleton);
            }
        }
    }
    return &singleton->instance_;
}

BINARY HACKS本での実装

書籍「BINARY HACKS ハッカー秘伝のテクニック100選」(高林哲、鵜飼文敏、佐藤祐介、浜地慎一郎、首藤一幸 著)より

HACK#37で「C++でシングルトンを実装する」というお題でマルチスレッド対応の4つの実装方法を紹介しています。

  1. Mutexでアクセスのたびにロックする方法
  2. ダブルチェック度ロッキングを使う方法
  3. TLS(スレッドローカルストレージ)を使う方法
  4. GCC固有機能の-fthreadsafe-staticsを使う方法