C++におけるプログラム中で実体が一つだけ必要な場合の実装例を調べてみました。特に、GitHubでStar数が多いものや開発が活発なものを対象としました。プログラムで共通した変数を参照するConfigやLoggerなどを想定しています。
ただ、グローバル変数とexternを利用したものや、main関数のローカル変数を引数でバケツリレーするようなものは省きました。また、実質的に機能が同じものもありますが、利用ケースが違うため、別物として扱っています。
C言語寄り
内部リンケージとインターフェース関数
C++では、無名名前空間またはstaticで定義した、静的オブジェクトのリンケージはファイル内に限定されます。実装方法がシンプルで、実体は呼び出し側から完全に隔離されています。しかしながら、異なる翻訳単位で定義されたローカルでない静的オブジェクトは、生成と破棄の順番を決めることができません。
ヘッダファイル
namespace config { void init() ; void set(const std::string& key, int val) ; int get(const std::string& key) ; }
ソースファイル
namespace { std::unordered_map<std::string, int> g_config ; } namespace config { void init() { g_config.clear() ; } void set(const std::string& key, int val) { g_config[key] = val ; } int get(const std::string& key) { return g_config.at(key) ; } }
関数内の静的変数
C++では、ローカルに定義された静的変数は初回呼び出し時に生成されるため、呼び出しの順番をある程度制御することができます。パスのように、一回だけ初期化すればよいものを順序だてて定義したい場合に非常に有効です。Effective C++の4項などでも紹介されている実装方法で、シングルトンのget_instanceでも頻繁に利用されるものです。ただ、publicなコンストラクタによる生成を制限していないので、完全なシングルトンではありません。
次の例では、constを付与することで、変数を完全にRead-onlyな状態にしています。
const auto& path1() { static const std::filesystem::path& tmp = read_path_from_config() ; return tmp ; } const auto& path2() { static const std::filesystem::path& tmp = path1() / "bar.txt" ; return tmp ; }
この場合、静的変数がpath1→path2の順で生成されることが保証されます。さらに、スレッドセーフであることが保証されています。(ref. ブロックスコープを持つstatic変数初期化のスレッドセーフ化 - cpprefjp C++日本語リファレンス)
C++寄り
シングルトンを利用したものや、クラスのstaticメソッドを利用したものです。
ユーティリティクラス
staticメンバとstaticメソッドのみを持つクラスとして実装します。これは実質的に内部リンケージとインターフェース関数のパターンと同じことをしています。
- 「クラスはオブジェクトを作るのが役割であり、名前空間を作るためにクラスを使うのはいかがなものか。名前空間という機能があるならばそちらを使うべきだ。」
- 「静的変数を利用するなら、templateで一括で一般化できるから汎用性が高い。」
- 「共通のデータを持つなら、名前空間のほうがそれを実装ファイルに隠ぺいできるので、結合が少ない。」
- 「staticクラスでもpimplイディオムやprivateで隠ぺいできる。」
- 「名前空間のほうが、ユーザが拡張できる。」
など、結構対立してるようです。
参考
- c++ - Namespace + functions versus static methods on a class - Stack Overflow
- Using a namespace in place of a static class in C++? - Stack Overflow
- Are utility classes with nothing but static members an anti-pattern in C++? - Software Engineering Stack Exchange
PowerToysでは、サードパーティのロガーをstaticなstd::shared_ptrとして確保していました(ref. PowerToys/logger.h at main · microsoft/PowerToys · GitHub)。ほかにも、ストリームのオブジェクトを保持して書き込んでいるプロジェクトもちらほら見かけました。次がその一例です。
class Foo { private: static std::ofstream ofs_ ; public: Foo() = delete ; static void init() { ofs_.open("log.txt") ; } static log(const std::string& msg) { ofs_ << msg ; ofs_.flush() ; } } ;
ただ、ローカルでない静的変数は初期化の順番が未定義であるため、他のローカルでない静的変数の初期化中に参照されていた場合にはアクセス違反となる可能性があります。その点、シングルトンは安全です。
関数の静的変数を利用したシングルトン
C++におけるもっともスタンダードなシングルトンです。C++11以降であればスレッドセーフに利用でき、書き方も非常にシンプルです。生成のタイミングは初回呼び出し時となります。
class Foo { private: std::unordered_map<std::string, int> config_ ; Foo() = default ; ~Foo() = default; public: static Foo& get_instance() { static Foo instance ; return instance ; } void set(const std::string& key, int val) { config_[key] = val ; } auto get(const std::string& key) { return config_.at(key) ; } } ;
C++11より前であれば、ブロックスコープを持つstatic変数初期化のスレッドセーフ化 - cpprefjp C++日本語リファレンスで述べられているようなDouble Checked Lockingというテクニックを用いて対策を行います。
このほかにも、staticメンバとしてプログラム開始時に実体を確保したり、グローバル変数の実体を参照するようなシングルトンも見られました。
ポインタを利用したシングルトン
こちらも、比較的スタンダードなシングルトン実装です。生成と解放のタイミングを制御できるのが特徴です。
class Foo { private: std::unordered_map<std::string, int> config_ ; static Foo* instance_ ; Foo() ; public: static Foo& get_instance() { if(!instance_) { instance_ = new Foo() ; } return *instance_ ; } static void destroy_instance() { if(instance_) { delete instance_ ; instance_ = nullptr ; } } void set(const std::string& key, int val) { config_[key] = val ; } auto get(const std::string& key) { return config_.at(key) ; } } ;
こちらは最後にどこかでdestroy_instanceを呼び出す必要があります。そこで、シングルトンのベターな実装方法 - Qiitaで解説されているようなmozc式シングルトンを利用したほうがいいケースもあります。ちなみに、インスタンスをスマートポインタに置き換えれば、最悪destroy_instanceが呼ばれなかった場合でも、static変数と同じように解放させることができます。
class Foo { private: std::unordered_map<std::string, int> config_ ; static std::unique_ptr<Foo> instance_ ; Foo() ; public: static Foo& get_instance() { if(!instance_) { instance_ = std::make_unique<Foo>() ; } return *instance_ ; } static void destroy_instance() { instance_.reset() ; } void set(const std::string& key, int val) { config_[key] = val ; } auto get(const std::string& key) { return config_.at(key) ; } } ;
ローカルな静的変数を動的に確保するシングルトン
bitcoinで利用されているものです(ref. bitcoin/logging.cpp at master · bitcoin/bitcoin · GitHub)。動的に確保した実体の解放をOSに頼り、実質的に実体を最後まで生存させる実装パターンです。とりわけ、staticな変数のデストラクタ内でもロガーを呼び出すことができます。ただし、一部のLinuxディストリビューションや組み込みOSの中には、プロセス終了時にメモリが解放されないものがあるため注意が必要です。
class Foo { private: std::ofstream ofs_ ; Foo(std::filesystem::path file) : ofs_(file) {} ~Foo() = default ; public: static Foo& get_instance() { static auto instance = new Foo("log.txt") ; return *instance ; } static log(const std::string& msg) { ofs_ << msg ; ofs_.flush() ; } } ;
ローカル変数を参照するシングルトン
寿命を任意のスコープ内に規定できる点が優れていますが、アクセスの可否が外部に依存しており、少し危険な実装です。実体が解放されている状態でアクセスすると、未定義となります。厳密にはシングルトンに分類されないかもしれません。
class Foo { private: std::unordered_map<std::string, int> config_ ; static Foo* instance_ ; public: Foo() { if(!instance_) { instance_ = this ; } } ~Foo() { if(instance_) { instance_ = nullptr ; } } static Foo& get_instance() { if(instance_) { return *instance_ ; } // error } void set(const std::string& key, int val) { config_[key] = val ; } auto get(const std::string& key) { return config_.at(key) ; } } ;
GUIにおけるメインウィンドウのメンバ変数として持てば、スマートポインタのような運用ができそうです。
void load() { Foo::get_instance().set("hoge", 2) ; } void print() { std::cout << Foo::get_instance().get("hoge") << std::endl ; } int main() { Foo bar ; load() ; print() ; return 0 ; }
まとめ
最後に解放タイミングやインターフェースなどの観点から比較しました。
実装パターン | 確保のタイミング | 解放のタイミング | インターフェース |
---|---|---|---|
内部リンケージとI/O関数 | プログラム起動時(順不定) | プログラム終了時(順不定) | func() |
関数内の静的変数 | 初回の呼び出し時 | プログラム終了時(順不定) | func() |
ユーティリティクラス | 明示的な初期化 | プログラム終了時(順不定) | Foo::func() |
静的な実体を利用したシングルトン | 初回の呼び出し時 | プログラム終了時(順不定) | Foo::get_instance().func() |
ポインタを利用したシングルトン | 初回の呼び出し時 | 明示的な解放 | Foo::get_instance().func() |
関数内の静的変数を動的に確保するシングルトン | 初回の呼び出し時 | プロセス終了時 | Foo::get_instance().func() |
ローカル変数を参照するシングルトン | ローカル変数の定義時 | ローカル変数の解放時 | Foo::get_instance().func() |
ほかにも有名な実装方法や、おすすめのデザインパターンがあればコメントで教えてください。GitHubなどのコードの参照があれば理解の助けになります。