開発を行っていると、複数のファイルやクラスで同じ情報を参照したい状況があると思います。
しかし、これをどこからでも見れるグローバル変数で管理すると、複数箇所から勝手に変更される危険性があります。
シングルトンの仕組みを使用することで、データがただ一つであることを保証しつつ、カプセル化などオブジェクト指向の機能を利用することができます。
本記事では、シングルトンの考え方と、サンプルコードについて紹介します。
前提知識
-
C++の基礎構文を理解している
シングルトン/Singletonとは
シングルトンとは、あるクラスのインスタンスがアプリケーション全体でただ一つだけ存在することを保証するデザインパターンのことです。
グローバル変数のようにどこからでもアクセスできるが、カプセル化は行いたいといった際に使用されるパターンです。
簡便である反面で、密結合になりやすく責務が曖昧になりがちになるといった特徴から、長期的にはアンチパターンとされることが多いです。
グローバル変数やstaticクラスとの比較
似た実装を可能にするグローバル変数やstaticクラスとの比較を行います。
1. グローバル変数
-
特徴
- プログラム全体から直接アクセス可能
- 初期化は一度だけで寿命はプログラム終了まで
-
問題点
- どこからでも変更できるため、予期せぬバグの原因になりやすい
- テストや保守が難しくなる
2. static クラス
-
特徴
- インスタンス不要で呼び出せる
- 状態を持たないユーティリティ関数の集約に便利
-
問題点
- 状態を持たせるとグローバル変数と同じ危険性がある
- 継承やインターフェース実装ができず、柔軟性に欠ける
3. シングルトン
-
特徴
-
唯一のインスタンスを保証
-
メンバメソッドを通じてアクセス制御が可能
-
-
メリット
- グローバル変数より安全に共有状態を管理できる
- オブジェクト指向的に責務を閉じ込められる
-
注意点
- 密結合になりやすく、テストが難しい
- 過剰に使うとアンチパターン化する
| 項目 | グローバル変数 | static クラス | シングルトン |
|---|---|---|---|
| インスタンス管理 | なし | なし | 唯一のインスタンス |
| 状態保持 | 可能だが危険 | 可能だが危険 | 安全に制御可能 |
| カプセル化 | なし | 限定的 | 強い |
| 拡張性 | 低い | 低い | インターフェース実装可 |
| テスト性 | 困難 | 困難 | 困難ではないが容易でもない |
この三つでしたら、一般的には、グルーバル変数はなるべく使用しない、ユーティリティ関数の集約にはstaticクラスを、状態を持つ場合にはシングルトンを使用する。
というのが理にかなった使い分けといわれています。
コード例
シングルトンでのゲーム音量の管理をイメージしたコード
//-------------------------------------------------------------
//! @file GameSetting.h
//! @brief ゲーム設定管理シングルトンクラス定義
//-------------------------------------------------------------
#pragma once
//-------------------------------------------------------------
// ゲーム設定を管理するシングルトンクラス
//-------------------------------------------------------------
class GameSettings {
private:
int volume; // 音量
private:
//-------------------------------------------------------------
// コンストラクタ
//! @note privateにすることで、外部から new でインスタンスを作れないようにする
//-------------------------------------------------------------
GameSettings();
//-------------------------------------------------------------
// コピーコンストラクタと代入演算子を禁止
//-------------------------------------------------------------
GameSettings(const GameSettings&) = delete;
GameSettings& operator=(const GameSettings&) = delete;
public:
//-------------------------------------------------------------
// 唯一のインスタンスを返す関数
//! @return GameSettings の唯一のインスタンスの参照
//! @note 初回呼び出し時に static 変数が生成される
//-------------------------------------------------------------
static GameSettings& Instance();
//-------------------------------------------------------------
// 音量の設定
//! @param v [in] 音量
//-------------------------------------------------------------
void SetVolume(int v);
//-------------------------------------------------------------
// 音量の取得
//! @return 音量
//-------------------------------------------------------------
int GetVolume() const;
};
#include "GameSettings.h"
//-------------------------------------------------------------
//! @brief コンストラクタ
//-------------------------------------------------------------
GameSettings::GameSettings()
: volume(128) {
}
//-------------------------------------------------------------
//! @brief 唯一のインスタンスを返す関数
//-------------------------------------------------------------
GameSettings& GameSettings::Instance() {
//static変数は一度だけ初期化される
static GameSettings instance;
return instance;
}
//-------------------------------------------------------------
//! @brief 音量の設定
//-------------------------------------------------------------
void GameSettings::SetVolume(int v) {
//0~255の範囲に制限
if (v < 0) {
volume = 0;
} else if (v > 255) {
volume = 255;
} else {
volume = v;
}
}
//-------------------------------------------------------------
//! @brief 音量の取得
//-------------------------------------------------------------
int GameSettings::GetVolume() const {
return volume;
}
現在の音量: 200
とりあえず、音量の設定は行えました。
staticメソッドのスコープ内でstaticなインスタンスを生成し、それを返すことで唯一のインスタンスを触ることができます。
staticなローカル変数は一度だけ初期化され、二度目以降に呼び出された際にも初めて呼び出された時と同じものが参照されます。
//-------------------------------------------------------------
//! @brief 唯一のインスタンスを返す関数
//-------------------------------------------------------------
GameSettings& GameSettings::Instance() {
//static変数は一度だけ初期化される
static GameSettings instance;
return instance;
}
main.cppを少し変更して、本当にインスタンスが作成できないか試してみます。
#include <iostream>
#include "GameSettings.h"
//メイン関数
int main() {
//インスタンスの生成を試みる(コンパイルエラーになる)
GameSettings settings; // エラー: コンストラクタが private のためアクセス不可
return 0;
}
"GameSettings::GameSettings()" (宣言された 行 17 、ファイル名 ".......GameSettings.h") にアクセスできません
'GameSettings::GameSettings': private メンバー (クラス 'GameSettings' で宣言されている) にアクセスできません。
確かに、インスタンスを作成することができないようです。
これは、コンストラクタをprivateにすることで、外部でのインスタンス作成を制限することができています。
private:
//-------------------------------------------------------------
// コンストラクタ
//! @note privateにすることで、外部から new でインスタンスを作れないようにする
//-------------------------------------------------------------
GameSettings();
コピーや代入を禁止しないと、唯一インスタンスのはずのものをコピーできてしまうので、禁止しています。
main.cppを少し編集して例をあげてみます。
#include <iostream>
#include "GameSettings.h"
//メイン関数
int main() {
//コピーを試みる(コンパイルエラーになるはず)
GameSettings copy = GameSettings::Instance(); // コンパイルエラー
return 0;
}
"GameSettings::GameSettings(const GameSettings &)" (宣言された 行 22 、ファイル名 "C:.......GameSettings.h") にアクセスできません
'GameSettings::GameSettings(const GameSettings &)': 削除された関数を参照しようとしています
禁止にしている場所はGameSettingsクラスの宣言で、メンバ関数の宣言時に = deleteとすることで、関数を削除することができます。
//-------------------------------------------------------------
// コピーコンストラクタと代入演算子を禁止
//-------------------------------------------------------------
GameSettings(const GameSettings&) = delete;
GameSettings& operator=(const GameSettings&) = delete;
このようにして、シングルトンではインスタンスを唯一に保証しています。
総括
-
シングルトンを使用することで、オブジェクト指向を使いながら複数の処理で共通で持ちたい情報を管理できる。 -
シングルトンの多用は依存性の問題からアンチパターンにもなるため注意が必要 - コピーの禁止や
コンストラクタをprivateにすることで、外部からインスタンスを作成できないことを保証することが出来る。