GoFデザインパターン - シングルトン編
GoFコレクションにおけるシングルトンパターン実践ガイド:C++サンプル、スレッドセーフに関する注記、使用例、アンチパターン
こんにちは!パン君です。
今回は GoF(Gang of Four)のデザインパターンのシングルトン編になります。
サンプルコード(C++)、作り方、使うタイミング、注意点、mermaid図による可視化などを含めて実用的に解説します。
はじめに
シングルトンはあるクラスのインスタンスがアプリケーション内でただ1つだけ存在することを保証し
その単一インスタンスへのグローバルなアクセス点を提供するためのデザインパターンです。
主なシングルトンの要素としては基本的に下記が満たされているものを差します。
- コンストラクタがプライベート : 外部からの直接的なインスタンス生成を防ぐ
- 静的なインスタンス : クラス内で自身を保持する
- 静的な取得メソッド : 静的インスタンスを外部に提供する
Wikipedia の定義は下記です
"ソフトウェア工学において、シングルトンパターンとは、クラスのインスタンス化を「単一」のインスタンスに制限するソフトウェア設計パターンである。"
(出典: https://en.wikipedia.org/wiki/Singleton_pattern)
GoF の観点では オブジェクトの生成とアクセスを制御することで 共有資源(設定、ロガー、接続プールなど)に対して一元的に制御を掛けるためのデザインパターンになっています。
この記事のゴール:
- C++ における代表的な実装パターンとその利点・欠点を示す
- スレッドセーフな初期化、パフォーマンス、テスト性の観点からの設計上の注意点を説明する
- 代替手段(依存性注入など)とアンチパターンを整理する
参考となる一次情報(抜粋):
en.wikipedia.orgSingleton pattern - Wikipedia
en.cppreference.comStorage class specifiers - cppreference.com
en.cppreference.comstd::call_once - cppreference.com
以上を根拠に、以降の実装と設計上の解説を行います。
代表的なユースケース
- 設定の一元管理
- ロガーインスタンスの共有
- ハードウェアリソース/接続プールなど 唯一のインスタンスで十分なケース
- アプリケーション全域で共有されるキャッシュやファクトリ
ただし、シングルトンはグローバルインスタンスなのでテストしずらくなるケースがあります。
そのため 「 本当に必要か 」 「 グローバルにアクセスを許していい唯一のインスタンスなのか 」 を常に検討してください。
C++ 実装パターン
シングルトンパターンにもいくつかのパターンの分類があります。
- Eager Initialization Singleton (即時初期化)
- Lazy Initialization Singleton (遅延初期化)
- Double Checked Locking Singleton (DCL Singleton)(2重チェックロッキング)
- Initialization on Demand Holder (ホルダー初期化)
などなど
以下にいくつかのC++実装を示します。
コードは学習用途に簡潔化しています。
Eager Singleton実装
Eager Singleton は実装が凄く簡単です。
ただ下記サンプルコードの場合はコメント記載している通り、プログラム起動時に生成されるためそのフェーズの処理コストは発生します。
シングルトン実装のケースは基本これで耐えうるともいます。
// EagerSingleton.h
#pragma once
#include <iostream>
class EagerSingleton {
private:
static EagerSingleton instance; // プログラム起動時に生成される
EagerSingleton() = default;
public:
EagerSingleton(const EagerSingleton&) = delete;
EagerSingleton& operator=(const EagerSingleton&) = delete;
static EagerSingleton& GetInstance() {
return instance;
}
void Say() { std::cout << "EagerSingleton\n"; }
};
// EagerSingleton.cpp
EagerSingleton EagerSingleton::instance; // instance の定義が必要Lazy Singleton実装
Lazy Singleton は mutex でスレッドセーフなシングルトンパターンです。
C++でこれを実現するには 取得の度にロックコストが発生するためパフォーマンスへの懸念があります。
下記2点どちらかがあるなら実装方向で進めるべきかもしれません。
- パフォーマンス削ってもいい
- 使用言語によっては懸念にならない
// LazySingleton.h
#pragma once
#include <mutex>
#include <memory>
class LazySingleton {
private:
static std::mutex mtx;
static LazySingleton* instance;
LazySingleton() = default;
public:
LazySingleton(const LazySingleton&) = delete;
LazySingleton& operator=(const LazySingleton&) = delete;
static LazySingleton* GetInstance() {
std::lock_guard<std::mutex> Lock(mtx);
if (!instance) instance = new LazySingleton();
return instance;
}
void Say();
};
// LazySingleton.cpp
std::mutex LazySingleton::mtx;
LazySingleton* LazySingleton::instance = nullptr;Meyers' Singleton実装
Meyers' Singleton は簡潔で軽く説明するとインスタンスの配置場所が下記の違いがあります
- Eager Singleton : クラスメンバーに変数配置
- Meyers' Singleton : 静的取得関数にローカルで変数配置
C++11以降では 静的局所変数の初期化 はスレッドセーフであるため C++では Eager Singleton より推奨される実装方法です
en.cppreference.comStorage class specifiers - cppreference.com
// MeyersSingleton.h
#pragma once
#include <iostream>
class MeyersSingleton {
private:
MeyersSingleton() { }
public:
MeyersSingleton(const MeyersSingleton&) = delete;
MeyersSingleton& operator=(const MeyersSingleton&) = delete;
static MeyersSingleton& GetInstance() {
static MeyersSingleton instance; // C++11以降でスレッド安全に初期化される
return instance;
}
void Say() { std::cout << "MeyersSingleton\n"; }
};std::call_once を使った Lazy Singleton実装
これは複雑な初期化が必要な場合に明示的で安全な手段です
実装工数と可読性的に使用頻度はすくないかもですが
en.cppreference.comstd::call_once - cppreference.com
// CallOnceSingleton.h
#pragma once
#include <mutex>
#include <memory>
class CallOnceSingleton {
private:
static std::unique_ptr<CallOnceSingleton> instance;
static std::once_flag initFlag;
CallOnceSingleton() = default;
static void Init() {
instance.reset(new CallOnceSingleton());
}
public:
CallOnceSingleton(const CallOnceSingleton&) = delete;
CallOnceSingleton& operator=(const CallOnceSingleton&) = delete;
static CallOnceSingleton& GetInstance() {
std::call_once(initFlag, &CallOnceSingleton::Init);
return *instance;
}
void Say();
};
// CallOnceSingleton.cpp
std::unique_ptr<CallOnceSingleton> CallOnceSingleton::instance;
std::once_flag CallOnceSingleton::initFlag;C++のシングルトンパターンの応用
パターンの説明とは違いますが、このデザインパターンの基本を理解したうえで使用すると様々な応用が可能です。
よくある応用ケースですが雑にいくつか紹介してみます。
あくまで例なので最適解でも正解でもなんでもなく、固定概念に捕らわれないようにするためのサンプルです。
どうやったらより良くできるか等、思考錯誤できるようがんばってください。
Template Eager Singleton
これだとシングルトンパターンを毎回実装する必要がなくなります
その代わり Foo.h を見た時にシングルトンかどうか判断は出来なくなるので可読性が上がっているようで、下がっているようにも見れます
※「じゃあどうよう」に対する改善は是非チャレンジしてみてください!
// TemplateEagerSingleton.h
#pragma once
#include <iostream>
template<typename T>
class TemplateEagerSingleton {
private:
static T instance; // プログラム起動時に生成される
TemplateEagerSingleton() = delete;
public:
TemplateEagerSingleton(const TemplateEagerSingleton&) = delete;
TemplateEagerSingleton& operator=(const TemplateEagerSingleton&) = delete;
static T& GetInstance() {
return instance;
}
};
// static メンバの定義(テンプレートなのでヘッダ内に置く)
template<typename T>
T TemplateEagerSingleton<T>::instance;
// Foo.h
struct Foo {
void Say() { std::cout << "TemplateEagerSingleton MyService\n"; }
};
// other.cpp
auto& foo = TemplateEagerSingleton<Foo>::GetInstance();
foo.Say();Multi Instance Eager Singleton
複数のグローバルアクセス可能なインスタンスにアクセスできるようになります
マジックナンバーとかその辺のどうとでもなるところは今回無視するとして問題は ライフサイクルの管理 と 使用用途の疑問 くらいです。
ライフサイクルの管理はケースによるのでなんともですが
使用用途の幅を増やすならポリモーフィズムになるようにコンパイル時定数で内部データ可変にしたリ等すればやるメリットが生まれてくると思います。
この辺もやりようはあると思うのでケースバイケースです。
// MultiInstanceEagerSingleton.h
#pragma once
#include <array>
#include <iostream>
#include <cstddef>
#include <cassert>
class MultiInstanceEagerSingleton {
private:
static constexpr std::size_t COUNT = 3;
static std::array<MultiInstanceEagerSingleton, COUNT> instances; // プログラム起動時に複数生成される
int id_;
MultiInstanceEagerSingleton(int id = 0) : id_(id) {}
public:
MultiInstanceEagerSingleton(const MultiInstanceEagerSingleton&) = delete;
MultiInstanceEagerSingleton& operator=(const MultiInstanceEagerSingleton&) = delete;
// index を指定して複数インスタンスへアクセスする
static MultiInstanceEagerSingleton& GetInstance(std::size_t index) {
assert(index < COUNT && "index out of range");
return instances[index];
}
void Say() { std::cout << "MultiInstanceEagerSingleton id=" << id_ << '\n'; }
};
// MultiInstanceEagerSingleton.cpp
#include "MultiInstanceEagerSingleton.h"
// 静的メンバの定義(プログラム起動時に COUNT 個のインスタンスが生成される)
std::array<MultiInstanceEagerSingleton, MultiInstanceEagerSingleton::COUNT> MultiInstanceEagerSingleton::instances = {
MultiInstanceEagerSingleton(0),
MultiInstanceEagerSingleton(1),
MultiInstanceEagerSingleton(2)
};
// other.cpp
auto& s0 = MultiInstanceEagerSingleton::GetInstance(0);
s0.Say();使用するタイミングと代替アプローチ
シングルトンを採用する前に次の点を検討してください
- 本当に 「1つだけのインスタンス」 である必要があるか
- テストでインスタンスを差し替えたいか
- マルチスレッド環境での初期化の安全性は確保されているか
代替案は下記です
- 依存性注入(Dependency Injection) : 呼び出し側に依存オブジェクトを渡すことで、テスト時にモックやスタブと差し替えやすくなる
- ファクトリ/サービスロケータ : 柔軟性を高める手段。 ただし、後者はグローバル依存を隠蔽するため注意が必要
- モジュール単位のシングルトン : C++では オブジェクトをモジュール(名前空間/翻訳単位)で管理する ことで十分な場合がある
テストのしやすさ
シングルトンはグローバル状態を持つため、ユニットテストでは状態のリセットや並列テストへの影響に注意が必要です
対処例は下記です。
- テスト専用の初期化/破棄 API を用意する
- 可能ならコンストラクションを外部から提供し、テスト時はモックを注入する
- Meyers' Singleton のような関数内静的変数はプロセス終了まで生きるため、走行中に状態を戻すのが難しい
アンチパターンとよくある落とし穴
- 過剰な使用 : 単に「使い回したい」という理由で乱用すると、コードの結合度が高くなる
- グローバル状態化 : 隠れた依存関係が増え、テストやリファクタリングが困難になる
- ライフサイクル管理の不明確さ : プログラム終了時のリソース解放や順序問題、静的オブジェクト同士の依存関係に注意
まとめ
- シングルトンは「ただ1つのインスタンス」を保証する便利なパターンだが、設計上のトレードオフを必ず考慮する必要があります
- C++ では C++11 以降の関数内静的変数(Meyers')がお手軽で安全な実装方法の一つである
- 明示的な制御が必要な場合は
std::call_onceを使うと良い - 大規模なシステムやテスト性が重視されるシステムでは、依存性注入やファクトリなどの手法を検討することを推奨する
参考・出典を記します
en.wikipedia.orgSingleton pattern - Wikipedia
en.cppreference.comStorage class specifiers - cppreference.com
en.cppreference.comstd::call_once - cppreference.com
それでは、次回は別の GoF パターンについても解説していきます。
この記事で紹介した実装は学習目的に簡潔化しています。
実プロダクションで採用する際は要件に合わせて十分に検討してください
以上、パン君でした!
コメントを読み込み中...