singletonなんてタグがあるくらい、みんな大好きシングルトン。
ところで、あなたのシングルトンは本当にそんな実装で大丈夫ですか?
今回は、単純なようで奥が深い、シングルトンの実装について考えてみます。
シングルトンとは
GoFによって定義されたデザインパターンのひとつ。
Singleton パターン
実装方法
シングルトンの実装方法は、大きく分けて2通り存在します。
- ローカル静的オブジェクトによる実装
- 動的割り当てによる実装
ローカル静的オブジェクトによる実装
class Foo {
private:
Foo() = default;
~Foo() = default;
public:
Foo(const Foo&) = delete;
Foo& operator=(const Foo&) = delete;
Foo(Foo&&) = delete;
Foo& operator=(Foo&&) = delete;
static Foo& get_instance() {
static Foo instance;
return instance;
}
};
Wikipediaで紹介されている実装例と同じです。
ローカル静的オブジェクトによって、初めて Foo::get_instance() が呼び出されたタイミングで、インスタンス生成が行われます。
インスタンスの解放はプログラムが終了した後に自動的に行われます。
動的割り当てによる実装
class Foo {
private:
Foo() = default;
~Foo() = default;
static Foo* instance;
public:
Foo(const Foo&) = delete;
Foo& operator=(const Foo&) = delete;
Foo(Foo&&) = delete;
Foo& operator=(Foo&&) = delete;
static Foo& get_instance() {
return *instance;
}
static void create() {
if (!instance) {
instance = new Foo;
}
}
static void destroy() {
delete instance;
instance = nullptr;
}
};
Foo* Foo::instance = nullptr;
明示的に Foo::create() が呼び出された際に、動的にインスタンス生成を行います。
インスタンスの解放は、やはり明示的に Foo::destroy() を呼び出した時になります。
問題点
2つの実装法にはそれぞれ問題点があります。
ローカル静的オブジェクトによる実装の問題点
これは、オブジェクトの解放タイミングを管理することができないの一言に尽きます。
一般的にシングルトンオブジェクトの解放の順序は、生成と逆順になることが望ましいです。
visual studioなどは解放の順序を生成の逆順で行うことを保証します。
しかし、解放のタイミングがプログラムの終了後になってしまうため、解放処理を制御したい場合に困る状況があり得ます。
動的割り当てによる実装の問題点
こちらはローカル静的オブジェクトによる実装の問題点を見事にクリアしています。
この実装では、解放処理は明示的に書く必要があるため、プログラマが適切なタイミングで生成と逆順に解放していることを保証します。
本当にそうでしょうか?
動的割り当てによる実装の問題点は、解放処理をプログラマが手で書く必要があるところです。
共通の問題点
どちらの実装にも言えることですが、シングルトンクラスを書くたびに上記のようなインターフェースを書くのは面倒です。
解決方法
これらの問題を解決する手段として、mozc式シングルトンをお勧めします。
mozcはGoogle IMEです。Ubuntuなどを使っていると、iBusがアレなせいで使っている人もいるかと思います。
変更履歴
- 2020/04/10 addFinalizer() に排他制御を追加
#pragma once
#include <cassert>
#include <mutex>
class SingletonFinalizer {
public:
using FinalizerFunc = void(*)();
static void addFinalizer(FinalizerFunc func);
static void finalize();
};
template <typename T>
class Singleton final {
public:
static T& get_instance() {
std::call_once(initFlag, create);
assert(instance);
return *instance;
}
private:
static void create() {
instance = new T;
SingletonFinalizer::addFinalizer(&Singleton<T>::destroy);
}
static void destroy() {
delete instance;
instance = nullptr;
}
static std::once_flag initFlag;
static T* instance;
};
template <typename T> std::once_flag Singleton<T>::initFlag;
template <typename T> T* Singleton<T>::instance = nullptr;
#include "singleton.h"
namespace {
constexpr int kMaxFinalizersSize = 256;
std::mutex gMutex;
int gNumFinalizersSize = 0;
SingletonFinalizer::FinalizerFunc gFinalizers[kMaxFinalizersSize];
}
void SingletonFinalizer::addFinalizer(FinalizerFunc func) {
std::lock_guard<std::mutex> lock(gMutex);
assert(gNumFinalizersSize < kMaxFinalizersSize);
gFinalizers[gNumFinalizersSize++] = func;
}
void SingletonFinalizer::finalize() {
std::lock_guard<std::mutex> lock(gMutex);
for (int i = gNumFinalizersSize - 1; i >= 0; --i) {
(*gFinalizers[i])();
}
gNumFinalizersSize = 0;
}
void main() {
Foo& gFoo = singleton<Foo>::get_instance();
Bar& gBar = singleton<Bar>::get_instance();
SingletonFinalizer::finalize();
}
mozcのシングルトンクラスはテンプレートを使用することで、どんなクラスでもシングルトン化することができるようになっています。
解放処理は SingletonFinalizer::finalize() を適切なタイミングで呼び出すことで行います。
また、このクラスが必ずシングルトンオブジェクトの生成と逆順でインスタンスの解放を行うことを保証します。