- 追加された行はこの色です。
- 削除された行はこの色です。
#counter
* スタックを使うアロケーターとshared_ptrのはなし [#ee6308c1]
この記事は、[[''C++ Advent Calendar 2013''>http://partake.in/events/91328710-3c7b-436e-bd4e-4d98d88333f9]] 6日目の記事です。
-Prev 5日目の記事 [[hotwatermorning>http://hoge.hoge]]
-Next 7日目の記事 [[srz_zumix>http://hoge.hoge]]
* shared_ptrを要求する邪悪なマネージャー [#ja8d4121]
C++11で標準実装された、参照カウンタ方式によるポインタ'shared_ptr'。
古来のC++でもboostライブラリにより利用かのなので、すっかりおなじみのスマートポインターです。
なにせ、newしてもdeleteしなくて良いというお手軽さ。誰からも参照されなくなると勝手にdeleteされるという、慎ましやかな動作で人気を博しています。
初期のboostライブラリから実装されていたこともあり、shared_ptrを使ったクラスライブラリも多いのですが、中にはイケてないクラスもあります。なにがイケてないかというと、
- shared_ptrをコンテナに入れて保持している
- シングルトン実装で、コンテナの解放はアプリケーション終了時である
- 格納されるクラスが、ポインタや参照で他のクラスオブジェクトと依存関係にある
これらの要件を満たしているクラスを使うと、便利でお手軽なshared_ptrはバグの温床と化し、未定義動作の暗黒面へと落ちて行きます。
たとえば、こんなクラスです
#sh(cpp){{
// イケてないタスクマネージャー
struct EvilTaskManager {
// 管理されるタスククラスのインターフェイスクラス
struct Task {
virtual void update() = 0;
};
// タスクリストをshared_ptrを使って保持
vector<shared_ptr<Task>> tasks_;
// タスクリストにタスクを追加
void addTask(shared_ptr<Task> task) {
tasks_.push_back(task);
}
// タスクリストのすべてのタスクのupdate()を呼び出す。
void doUpdate() {
for (auto& task : tasks_) task->update();
}
// タスクリストをクリア
void clear() {
tasks_.clear();
}
// シングルトン実装です
static EvilTaskManager& instance() {
static EvilTaskManager me;
return me;
}
};
}}
こんな風に使います
#sh(cpp){{
// 毎フレーム呼ばれるなんらかのタスク
struct MyTask : EvilTaskManager::Task {
ostream& os_; // メッセージ出力用のストリーム
MyTask(ostream& os) : os_(os) {}
// 毎フレームの処理
void update() override {
os_ << "update MyTask" << endl;
}
// デストラクタでもメッセージを出す
~MyTask() {
os_ << "MyTask is gone." << endl;
}
};
void main() {
ofstream outmsg("message.txt");
// MyTaskをタスクマネージャーに登録
EvilTaskManager::instance().addTask(new MyTask(outmsg));
// メインループのつもり
bool is_running = true;
while (is_running) {
// ここで、MyTaskのupdate()が毎フレーム呼ばれる
EvilTaskManager::instance().doUpdate();
// render and wait vsync etc...
// 終わる時は、is_running = falseとする
}
}
}}
MyTaskは、メッセージ出力用のストリームを受け取って、仕事をしながらメッセージを出します。
自分が消滅するときにも、ご丁寧にメッセージを出します。
しかし、このプログラムは動作しません。お分かりと思いますが、MyTaskのデストラクタが呼ばれるのは、main()関数を抜けた後です。
その時には、ofstream outmsgは忘却の彼方へ旅立っており、アクセスすると*良くない事*が起こります。
上記はシンプルな例なので解決策は簡単に施せますが、シングルトンのマネージャークラスが沢山あって、それぞれが開放される順序がプログラム構造的に不明確だったりすると、厄介な問題になります。(Modern C++ Design 6章 シングルトン参照)
特に、ゲームプログラムの場合はリソースの管理が複雑になりがちで、「クラスのデストラクタが呼ばれた時には、解放するべきコンテキストが存在していない」と言う事がよく起こります。
MyTaskの実装上の問題は、ostreamが使用不能になる前に、タスクマネージャーが終了すれば問題は解決します。たとえば、EvilTaskManager::instance().clear()を、main()関数のwhileの文の後に入れれば問題は発生しません。
#sh(cpp){{
while (is_running) {
// ここで、MyTaskのupdate()が毎フレーム呼ばれる
EvilTaskManager::instance().doUpdate();
// render and wait vsync etc...
// 終わる時は、is_running = falseとする
}
EvilTaskManager::instance().clear(); // これで解決!
}
}}