ゆとりーなの日記

日記的な事を書いて行くと思はれる

定期ポストのシングルトンタイム

 函数内のstatic変数の初期化中に例外が飛んだらどうなるかちょっと気になったのでやってみました。

#include <iostream>
#include <string>
#include <stdexcept>

typedef void *HWND;

HWND CreateWindow() {
  // どういうわけかウィンドウが作れないー 
  return nullptr;
}

class MainWindow {
 public:
  MainWindow() : window_(nullptr) {
    std::cout << "ウィンドウ作るのに挑戦!" << std::endl;
    window_ = CreateWindow();
    if (!window_) {
      throw std::runtime_error("ウィンドウの作成に失敗したでござる");
    }
  }

  // ウィンドウ操作に便利なメンバ群

  HWND getWindowHandle() const {
    return window_;
  }

 private:
  HWND window_;
};

void hoge() {
  static MainWindow window;
}

int main() {
  try {
    hoge();
  } catch (const std::exception &error) {
    hoge();
  }
  return 0;
}

 今回はWin32を律儀に書くのは面倒且つ冗長なので擬似コードです。はい。実行結果はうちのUbuntuさんではこうなりました。

ウィンドウ作るのに挑戦!
ウィンドウ作るのに挑戦!
terminate called after throwing an instance of 'std::runtime_error'
  what():  ウィンドウの作成に失敗したでござる
中止

 まぁ残当ですね。函数内のstatic変数のコンストラクタで例外が飛んだ後、もう一回そこを通った場合にはさっき作るのに失敗した残骸が使われるのではなく新しくコンストラクタが呼ばれますね。
 なんでこんな話をしたかというと、シングルトンでこんな感じのことが置きかねないからですね。ゲームを作るとしたら、ウィンドウは特殊な事情でもない限り一つでしょうからシングルトンにすることもあるかもしれないわけですね。staticなローカル変数を使う実装の場合は実質上と同じような現象が起きる得るわけです。
 これで何が問題になるかというと、例外が飛んだときのメッセージ表示にメッセージダイアログを使う場合ですね。多くの場合メッセージダイアログは親ウィンドウを指定するバージョンと指定しないバージョンが用意されています。親を指定するバージョンでは、メッセージダイアログが出ている場合は親ウィンドウを操作することが出来ず、親を指定しないバージョンではそんなことないみたいな感じになってるかと思います。それでもってゲームのメッセージダイアログはどちらかというと前者を使っている場合が多いように見受けられます。そっちの方が色々とおいしいことがあるんです。エラー表示してるときにゲームウィンドウ操作出来てもしょうがないですしね。しかしウィンドウを作る前に起きたエラーを表示するときにはこうもいきません。親がないのですから親を指定しないバージョンを使うしかないわけです。
 で、例えば次のような感じに作ったとするとよくないわけです。

#include <iostream>
#include <string>
#include <stdexcept>
#include <boost/noncopyable.hpp>

typedef void *HWND;

HWND CreateWindow() {
  return nullptr;
}

void MessageBox(HWND parent, const std::string &message) {
  std::cout << (parent ? "親がいるメッセージダイアログ:" :  "孤児メッセージダイアログ:") << message << std::endl;
}

class MainWindow {
 public:
  MainWindow() : window_(nullptr) {
    std::cout << "ウィンドウ作るのに挑戦!" << std::endl;
    window_ = CreateWindow();
    if (!window_) {
      throw std::runtime_error("ウィンドウの作成に失敗したでござる");
    }
  }

  void show() {
    std::cout << "ウィンドウを表示!" << std::endl;
  }

  // ウィンドウ操作に便利なメンバ群

  HWND getWindowHandle() const {
    return window_;
  }

 private:
  HWND window_;
};

class WindowSingleton : boost::noncopyable {
 public:
  static WindowSingleton &getInstance() {
    static WindowSingleton instance;
    return instance;
  }

  void show() {
    window_.show();
  }

  HWND getWindowHandle() const {
    return window_.getWindowHandle();
  }

 private:
  WindowSingleton() : window_() {}
  MainWindow window_;  
};

int main() {
  try {
    WindowSingleton::getInstance().show();
  } catch (const std::exception &error) {
    // ウィンドウ作成に失敗していたらgetWindowHandle()はnullptrをくれる?
    MessageBox(WindowSingleton::getInstance().getWindowHandle(), error.what());
  }
  return 0;
}

実行結果

ウィンドウ作るのに挑戦!
ウィンドウ作るのに挑戦!
terminate called after throwing an instance of 'std::runtime_error'
  what():  ウィンドウの作成に失敗したでござる
中止

 実際にこれを実行するとMessageBox内の文字列は表示されないですね。catch節のWindowSingleton::getInstance()内でもう一回ウインドウを作ろうとして冷静に失敗して例外が飛んで強制終了です。これはいけません。
 というわけで今回はコンストラクタ内で例外を飛ばしてはいけなかったということになるわけですね。コンストラクタ内ではウィンドウがちゃんと作られたかを判定するだけに留めておき、作成成功したかを問い合わせるメンバ変数を作り、それが失敗を示したらどこか別のところで改めて例外を投げるしかないでしょう。

#include <iostream>
#include <string>
#include <stdexcept>
#include <boost/noncopyable.hpp>

typedef void *HWND;

HWND CreateWindow() {
  return nullptr;
}

void MessageBox(HWND parent, const std::string &message) {
  std::cout << (parent ? "親がいるメッセージダイアログ:" :  "孤児メッセージダイアログ:") << message << std::endl;
}

class MainWindow {
 public:
  MainWindow() : window_(nullptr) {
    std::cout << "ウィンドウ作るのに挑戦!" << std::endl;
    window_ = CreateWindow();
  }

  void show() {
    std::cout << "ウィンドウを表示!" << std::endl;
  }

  // ウィンドウ操作に便利なメンバ群

  HWND getWindowHandle() const {
    return window_;
  }

 private:
  HWND window_;
};

class WindowSingleton : boost::noncopyable {
 public:
  static WindowSingleton &getInstance() {
    static WindowSingleton instance;
    return instance;
  }

  void init() {
    if (!initialized_) {
      throw std::runtime_error("ウィンドウの作成に失敗したでござる");
    }
  }

  void show() {
    window_.show();
  }

  HWND getWindowHandle() const {
    return window_.getWindowHandle();
  }

 private:
  WindowSingleton() : initialized_(false), window_() {
    if (window_.getWindowHandle()) {
      initialized_ = true;
    }
  }
  bool initialized_;
  MainWindow window_;  
};

void MyMessageBox(const std::string &message) {
  MessageBox(WindowSingleton::getInstance().getWindowHandle(), message);
}

int main() {
  try {
    // 初期化という名の成功判定
    WindowSingleton::getInstance().init();
    WindowSingleton::getInstance().show();
  } catch (const std::exception &error) {
    // ウィンドウ作成に失敗していたらgetWindowHandle()はnullptrをくれる!
    MessageBox(WindowSingleton::getInstance().getWindowHandle(), error.what());
  }
  return 0;
}

実行結果

ウィンドウ作るのに挑戦!
孤児メッセージダイアログ:ウィンドウの作成に失敗したでござる

 ウィンドウ作成成功判定を全てのメンバ函数に埋め込んでどの函数から呼ばれても大丈夫とかすると頭悪いのでおとなしくinit()メンバ函数を作るしかないのでしょう。こいつはウィンドウ作成に失敗していたら例外を投げます。今度はコンストラクタがちゃんと終わっているので普通にgetWindowHandle()が呼び出せて、ウィンドウ作成に失敗していたらnullptrをくれます。何はともあれ一件落着です。
 で、段々ややこしくなってくるのがこのシングルトンがどんどん拡張されてきたときのことですね。グラフィックやサウンドやら入力マネージャやらも普通は一つでいいのでこのシングルトンに纏めて持たせてしまおうと欲が出てきたときの話です。そうなるとこのシングルトンの名前はライブラリを象徴する名前に、例えばGameLibSingletonとかになってきそうです。そしてMessageBoxも生APIを晒しておくのは気持ち悪いと考えてGameLibSingleton内のstaticメンバ函数になってしまったとします。そうするとmain函数部は、

int main() {
  try {
    // 初期化という名の成功判定
    GameLibSingleton::getInstance().init();
    GameLibSingleton::getInstance().windowShow();
  } catch (const std::exception &error) {
    // 分岐はライブラリ内で完結
    // 果たしてこのコードは大丈夫なのか?
    GameLibSingleton::MessageBox(error.what());
  }
  return 0;
}

 いつの間にかGameLibSingletonのinitがこけて例外が飛んだ場合にもGameLibSingletonの函数が使えてる???みたいな不思議な現象があっという間に作れてしまいます。設計とは難しいものです。