閑古鳥

オールドプログラマの日記。プログラミングとか病気(透析)の話とか。

RAII を ScopeGuard を使って実践

RAII(Resource Acquisition Is Initialization)に関してはリンク先を参照してもらうとして、先日の ScopeGuard を使った RAII の実装 (怪しい表現だなあ) を、実際にやってみた。自分がちゃんと理解できているかを確認するための覚書。

CloseFile の省略

#include <boost/multi_index/detail/scope_guard.hpp>
#include <windows.h>
using namespace boost::multi_index::detail;
void f()
{
  HANDLE file = CreateFile("hoge.txt", GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

  scope_guard sg = make_guard(&CloseHandle, file);

  // ↓こんなことができる(Closeしなくていい)
  if(ReadFile(...) == FALSE) { return; }
  if(WriteFile(...) == FALSE) { return; }
}

return した時点で scope_guard のインスタンス (sg) が解放されて、コンストラクタで登録した処理 (CloseHandle) を呼んでくれる、という寸法。この例の正解は ScopeGuard なんか使わずに std::fstream を使え、なんでしょうが、まあ低レベルなものを使いたいこともあるでしょうし、そういう時には便利なのではないかと。今まではこの CloseHandle をデストラクタに記述したクラス (構造体) をいちいち定義していて煩わしい思いもしたのですが、これを有効活用できればかなり幸せになれそうです。 read, write などの関数呼び出しを考えるとクラスにラップしてカプセル化した方が良いパターンもあるでしょうが。

例外安全を保証するために使う

また、 ScopeGuard には dismiss というメンバ関数が用意されており、このメンバ関数を呼び出すと、オブジェクト解放時 (デストラクタ) で最初に登録した処理を実行しなくなります。何に使うのかと言えば、例外安全を保証するため。

例外安全 (Exception Safe) というのは、「とある処理の途中に例外が発生したことにより副作用が起こらない」こと。例えば new した後に throw することで delete が呼ばれずメモリリークが起こったり、処理が完了する前に終わってしまうことで処理中のデータが中途半端な状態になってしまうことがない、ということです。前者の場合は先の例を使ってリソースを適切に解放してやればそれで済むのですが、後者の場合は、処理に失敗したとき (throw されたとき) に、データを処理前の状態にロールバックさせてやる必要があります。

つまり ScopeGuard に「データを処理前の状態に戻す」処理を登録しておいて、処理が無事終了したらその処理をキャンセル (dismiss) する。で ScopeGuard が解放されるとき、 dismiss されていれば処理は成功しているので何もせず、されていなければ処理は失敗しているので、ロールバック処理を実行する、てな感じ。このキャンセルというのは、「解雇」って呼んだ方がよいのかしら……。

/*※ v_ は std::vector<int> 型の変数 */
v_.push_back(1234);
scope_guard sg = make_obj_guard(v_, &std::vector<int>::pop_back);

// ... 何か色々処理(途中で失敗すると throw されたりする)...

sg.dismiss(); // 成功したらこれを呼ぶ

先日紹介したページにあったコードそのものですが……。まず最初に vector に適当な値を追加 (push_back) し、直後にロールバック処理を登録します。 v_ を処理前の状態に戻すには最後に追加した要素を削除すればよいので、 pop_back メンバ関数を登録します。で、なにやら色々処理をする。もし途中で失敗し例外が throw された場合は、sg が解放される時にロールバック処理が実行され、 v_ の中身は処理前の状態に戻されます。何事も無く成功したら、 sg が解放される前に dismiss が実行され、何も行いません。これにより、処理は成功するか、失敗して元の状態に戻されるかのどちらかになるということが保証される、と。うーん、もっと見たまんまな例ってないものかな。

このやり方は例外安全以外にも、データベースを使うときなどにも使えそうですね。データベースなんて滅多に触らないので、あまり自分が使う機会はなさそうですが。ああ、データベースといえば、本社のあれ (何) も、いい加減作り直した方がいい気もするんだけどなぁ……。何も考えずに作ったからそのうち煙吐いたりして。

ちなみに例外安全については Exceptional C++(asin:4894712709) で触れられているので詳しく知りたい方はそちらを参照されたし。