C++のシングルトンパターンについて

AI実装検定のご案内

C++のシングルトンパターンとは、あるクラスのインスタンスをプログラム全体で1つだけ生成し、そのインスタンスを共有して使うためのデザインパターンです。

たとえば、ログ出力、設定管理、リソース管理など、アプリケーション全体で1つだけ存在すればよいオブジェクトに使われることがあります。

シングルトンを使うと、次のような形で同じインスタンスにアクセスできます。

Logger::instance().log("Application started");
Logger::instance().log("Something happened");

どこから呼び出しても、Logger::instance() は同じ Logger オブジェクトを返します。

ただし、シングルトンは便利な反面、グローバル変数に近い性質を持ちます。

使い方を誤ると、依存関係が見えにくくなったり、テストしづらいコードになったりするため、実務では慎重に使う必要があります。

目次

C++における基本的なシングルトン実装

C++でよく使われるシングルトンの基本形は、次のような実装です。

class Singleton {
public:
    static Singleton& instance() {
        static Singleton s;
        return s;
    }

private:
    Singleton() = default;

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

この実装では、instance() 関数の中にある関数ローカル static 変数によって、Singleton オブジェクトが1つだけ生成されます。

static Singleton s;

この s は、初めて instance() が呼ばれたときに生成されます。

その後、何度 instance() を呼んでも、同じ s への参照が返されます。

コンストラクタをprivateにする理由

シングルトンでは、外部から自由にインスタンスを作られないようにする必要があります。

そのため、コンストラクタを private にします。

private:
    Singleton() = default;

これにより、外部から次のようにインスタンスを生成できなくなります。

Singleton s; // エラー

もしコンストラクタが public であれば、誰でも自由にインスタンスを作れてしまいます。

それでは「インスタンスを1つだけに制限する」というシングルトンの目的を満たせません。

コピーを禁止する理由

シングルトンでは、コピーコンストラクタとコピー代入演算子も禁止する必要があります。

Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

これを書かないと、instance() が返したオブジェクトから別のオブジェクトをコピーできてしまう可能性があります。

Singleton copy = Singleton::instance(); // コピーコンストラクタが有効ならコピーできる

instance() が参照を返していても、コピーコンストラクタが利用可能であれば、参照先のオブジェクトから新しいオブジェクトをコピー生成できます。

そのため、シングルトンではコピーを明示的に禁止するのが基本です。

ムーブも禁止するとより明確になる

現代C++では、コピーだけでなくムーブも禁止しておくと、より意図が明確になります。

Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;

完成形としては、次のように書くと分かりやすいです。

class Singleton final {
public:
    static Singleton& instance() {
        static Singleton s;
        return s;
    }

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    Singleton(Singleton&&) = delete;
    Singleton& operator=(Singleton&&) = delete;

private:
    Singleton() = default;
};

final を付けることで、このクラスを継承できないことを明示できます。

シングルトンは継承と相性が悪いケースが多いため、継承させる予定がないなら final を付けてもよいでしょう。

Meyers Singletonとは

C++でよく使われる次のような実装は、一般的に Meyers Singleton と呼ばれます。

class Singleton {
public:
    static Singleton& instance() {
        static Singleton obj;
        return obj;
    }

private:
    Singleton() = default;

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

この実装は、シンプルでありながら実用性が高い点が特徴です。

Meyers Singletonのメリット

Meyers Singletonには、主に次のメリットがあります。

メリット内容
実装が簡単ポインタや動的確保を使わずに書ける
遅延初期化される初めて使われたときにインスタンスが生成される
C++11以降では初期化がスレッドセーフ複数スレッドが同時に呼んでも初期化は1回だけ行われる
メモリ管理が簡単通常は自分で newdelete を書く必要がない

特別な理由がなければ、現代C++ではこの形を基本に考えるのが無難です。

C++11以降では初期化がスレッドセーフ

C++11以降では、関数ローカル static 変数の初期化はスレッドセーフです。

つまり、複数のスレッドが同時に次の関数を呼んでも、

Singleton::instance();

static Singleton obj; は1回だけ安全に初期化されます。

これは、Meyers Singletonが現代C++でよく使われる大きな理由の1つです。

ただし、ここで注意したいのは、スレッドセーフなのはあくまで「初期化」です。

インスタンス生成後のメンバ関数呼び出しやメンバ変数の読み書きが自動的にスレッドセーフになるわけではありません。

シングルトンの利用例

シングルトンは、アプリケーション全体で共有したい処理やリソースに使われることがあります。

代表的な例として、ログ出力と設定管理があります。

ロガーの例

ログ出力クラスは、シングルトンの例としてよく使われます。

#include <fstream>
#include <mutex>
#include <string>

class Logger final {
public:
    static Logger& instance() {
        static Logger logger;
        return logger;
    }

    void log(const std::string& message) {
        std::lock_guard<std::mutex> lock(mutex_);
        file_ << message << std::endl;
    }

    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;
    Logger(Logger&&) = delete;
    Logger& operator=(Logger&&) = delete;

private:
    std::ofstream file_;
    std::mutex mutex_;

    Logger() : file_("app.log") {}
};

使い方は次の通りです。

int main() {
    Logger::instance().log("Program started");
    Logger::instance().log("Program finished");
}

この例では、Logger::instance() を通じて、プログラム全体で同じ Logger オブジェクトを使います。

また、log() 内で std::mutex を使っているため、複数スレッドから同時にログを書き込む場合にも、出力処理が競合しにくくなります。

設定管理の例

設定情報をアプリケーション全体で共有したい場合にも、シングルトンが使われることがあります。

#include <optional>
#include <string>
#include <unordered_map>

class Config final {
public:
    static Config& instance() {
        static Config config;
        return config;
    }

    void set(const std::string& key, const std::string& value) {
        values_[key] = value;
    }

    std::optional<std::string> get(const std::string& key) const {
        auto it = values_.find(key);

        if (it == values_.end()) {
            return std::nullopt;
        }

        return it->second;
    }

    Config(const Config&) = delete;
    Config& operator=(const Config&) = delete;
    Config(Config&&) = delete;
    Config& operator=(Config&&) = delete;

private:
    std::unordered_map<std::string, std::string> values_;

    Config() = default;
};

使い方は次のようになります。

int main() {
    Config::instance().set("mode", "debug");

    auto mode = Config::instance().get("mode");

    if (mode.has_value()) {
        // mode.value() を使う
    }
}

この例では、get() の戻り値を std::optional<std::string> にしています。

単純に空文字を返す実装も可能ですが、その場合、「設定値が空文字だった」のか「キーが存在しなかった」のかを区別できません。

std::optional を使うことで、値が存在するかどうかを明確に表現できます。

古いポインタベースの実装と問題点

以前は、次のようなポインタベースのシングルトン実装もよく見られました。

class Singleton {
private:
    static Singleton* instance_;

    Singleton() = default;

public:
    static Singleton* getInstance() {
        if (instance_ == nullptr) {
            instance_ = new Singleton();
        }

        return instance_;
    }
};

Singleton* Singleton::instance_ = nullptr;

一見すると、instance_nullptr のときだけ new Singleton() しているため、インスタンスは1つだけに見えます。

しかし、この実装にはいくつか問題があります。

メモリ管理が曖昧になる

この実装では、new Singleton() によって動的にメモリを確保しています。

instance_ = new Singleton();

しかし、どこで delete するのかが明確ではありません。

適切に解放しなければメモリリークになりますし、終了処理のタイミングを考える必要もあります。

一方、Meyers Singletonでは関数ローカル static を使うため、自分で newdelete を書く必要がありません。

マルチスレッドで二重生成される可能性がある

ポインタベースの単純な実装は、マルチスレッド環境で問題を起こす可能性があります。

if (instance_ == nullptr) {
    instance_ = new Singleton();
}

複数のスレッドが同時に getInstance() を呼んだ場合、両方のスレッドが instance_ == nullptr と判断して、それぞれ new Singleton() を実行してしまう可能性があります。

その結果、インスタンスが複数生成される可能性があります

以上、C++のシングルトンパターンについてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次