【C++】シングルトンのインスタンスの作り方|getInstanceの正しい書き方を解説

C++

今回は、C++でよく出てくる「シングルトンのインスタンスの作り方(getInstanceの正しい書き方)」について解説していきます。

getInstance() ってどう書くのが正解?」
new して返してるけど、これで合ってる?」
「解放はどうすればいいの?」

こんな疑問はありませんか?

シングルトンは「1つしか存在しない」ことを保証するパターンですが、その1つをどう作るかで書き方が何通りもあり、しかも古い書き方が今でも大量に検索に出てきます。ここを間違えるとメモリリークやクラッシュの原因になります。

この記事を読み終えると、あなたはgetInstanceの正しい書き方・古い書き方がダメな理由・スレッド安全性・初期化順序の罠をしっかり理解できると思いますので、ぜひ最後まで読んでいただけると嬉しいです。

なおシングルトンとは何か・どんな場面で使うかシングルトンパターンとは?の記事で解説しています。この記事はその一歩先、実装編です。

結論:静的ローカル変数で書く

先に答えを出します。C++11以降は、これが最もシンプルで安全です。

#include <iostream>

class GameManager {
public:
    // これが定番の書き方
    static GameManager& GetInstance() {
        static GameManager instance; // 初回呼び出し時に1度だけ作られる
        return instance;
    }

    void AddScore(int s) { score_ += s; }
    int  GetScore() const { return score_; }

private:
    GameManager() = default;  // 外から作れないように隠す
    ~GameManager() = default;

    // コピー・ムーブを禁止する(2個目を作らせない)
    GameManager(const GameManager&) = delete;
    GameManager& operator=(const GameManager&) = delete;

    int score_ = 0;
};

int main() {
    GameManager::GetInstance().AddScore(100);
    std::cout << GameManager::GetInstance().GetScore() << "n";
    return 0;
}
100

この書き方はMeyers Singleton(マイヤーズのシングルトン)と呼ばれます。ポイントは3つです。

  • 関数の中の static なので、初めて呼ばれたときに1度だけ作られる
  • プログラム終了時に自動で片付くdelete 不要)
  • コンストラクタを private にして、外から勝手に作れないようにする

static そのものの意味はstaticとは?の記事もあわせてどうぞ。

なぜ「newして返す」書き方はダメなのか

ネットでよく見かけるのが、こちらの書き方です。

// 古い書き方(おすすめしない)
class GameManager {
public:
    static GameManager* GetInstance() {
        if (instance_ == nullptr) {
            instance_ = new GameManager(); // 誰が delete する?
        }
        return instance_;
    }
private:
    static GameManager* instance_;
};

GameManager* GameManager::instance_ = nullptr;

動くには動きますが、次の問題があります。

  • 解放されないdelete を書く場所がなく、デストラクタも呼ばれない
  • スレッドセーフではない。複数スレッドから同時に呼ぶと2個作られる可能性がある
  • コードが長い。静的メンバの定義も別途必要

対して静的ローカル変数版は、C++11以降なら初期化が1度だけであることが規格で保証されており、スレッドセーフです。しかも短い。選ばない理由がありません。

使うときの書き方

参照を返しているので、呼び出し側はこう書けます。

// 毎回書くパターン
GameManager::GetInstance().AddScore(10);

// 何度も使うなら参照で受けると読みやすい
auto& gm = GameManager::GetInstance();
gm.AddScore(10);
gm.AddScore(20);

ここで必ず auto&(参照)で受けてくださいauto gm = ... と書くとコピーしようとしてコンパイルエラーになります(コピーを禁止しているため)。このエラーは正しく効いている証拠なので、慌てずに & を付ければ大丈夫です。参照については参照(&)とは?の記事をどうぞ。

【重要】私が実際にシングルトンで困った体験談

個人開発で、new して返す古い書き方のシングルトンを使っていたとき、終了時にセーブデータが書き込まれないという不具合にハマりました。

原因は、セーブ処理をデストラクタに書いていたこと。new したきり delete していないので、デストラクタが一生呼ばれないわけです。静的ローカル変数版に書き換えたら、終了時にきちんと呼ばれるようになりました。

もう一つ、もっと厄介だったのが初期化順序の問題です。SoundManager のデストラクタから LogManager を呼んでいたら、終了時にたまにクラッシュする。これは LogManager のほうが先に壊されていたためでした。

静的ローカル変数は「作られた順の逆」で壊れます。つまり後から作られたものが先に壊れる。デストラクタ同士で呼び合うと、この順序に振り回されます。シングルトンのデストラクタから他のシングルトンを触らない、と決めたら安定しました。

シングルトン実装のよくある失敗例と対処法

①コピーを禁止し忘れる

コピーコンストラクタを禁止しないと、GameManager copy = GameManager::GetInstance();2個目ができてしまいます。「1つだけ」が崩れるので、= delete を必ず書きましょう。

②デストラクタから他のシングルトンを触る

体験談のとおり、破棄順序に依存してクラッシュします。終了処理が必要なら、デストラクタ任せにせず明示的な Shutdown() を用意して、自分で順番を決めて呼ぶのが安全です。

③何でもシングルトンにする

便利なのでつい増やしがちですが、シングルトンは実質グローバル変数です。どこからでも書き換えられるので、増やすほどどこで壊れたか分からなくなります本当に1つしか存在しえないものだけに絞りましょう。

注意点

  • 実装は静的ローカル変数+参照を返すが定番
  • 受け取るときはauto&(参照)で
  • 終了処理はデストラクタ任せにしない

まとめ

  • getInstance関数内 static + 参照を返すのが正解
  • C++11以降はスレッドセーフが保証され、解放も自動
  • new して返す書き方は解放されず、スレッドセーフでもない
  • コピー禁止を忘れず、デストラクタ同士で呼び合わない

シングルトンは書き方さえ固定してしまえば、迷うところのないパターンです。この記事のテンプレートをそのままコピーして使ってみてください。

関連記事