Cだとgotoをつかうのが基本。C++だとtry/throw/catchもあり。
めんどくさいのが、atomicオペレーションを実装するときに、内部で副作用があちこちに発生して
途中で失敗したら取消てまわらないといけない場合。
正常系はこんなかんじだとして
Cのgotoパターン。
C++の例外処理をつかった例
おなじく例外処理をつかう別の例
C++0xで入るlambdaをつかった例
int main(int ac, char **av) { int rc = 1; int x = av[1] ? atoi(av[1]) : 0; /*atoiのエラーはみぬふり*/ f(x); /*エラーをおこすかも*/ g(x); /*エラーをおこすかも*/ return 0; }
Cのgotoパターン。
#include <iostream> #include <cstdlib> using std::cout; using std::endl; int a = 0; int b = 0; void show() { cout << "a=" << a << endl << "b=" << b << endl; } int f(int x) { if (x > 100) return -1; a += x; return 0; } void undo_f(int x) { a -= x; } int g(int x) { if (x > 10) return -1; b += x; return 0; } int main(int ac, char **av) { int rc = 1; int x = av[1] ? atoi(av[1]) : 0; if (f(x) < 0) goto err_f; if (g(x) < 0) goto err_g; rc = 0; goto out; err_g: undo_f(x); err_f: out: show(); cout << "rc=" << rc << endl; return rc; }g(x)が失敗したf(x)を取り消すためにundo_f(x)を呼び出す必要がある。 副作用をおこすブロック(この例だとf()とかg())毎に対応する gotoラベルを用意してリカバリルーチンを逆順に呼んでいく。 問題はf(),g()の部分とリカバリの部分がコード上で離れてしまうのと 逆順に書くの人間がやらないといけないところ。f()とg()の順序が逆になったときや f()とg()の間にh()が入ったときにリカバリ部分も書き換えないといけない。 たぶん書き換えるのは忘れないとおもうが、実装ミスをする個所が増える。
C++の例外処理をつかった例
#include <iostream> #include <cstdlib> using std::cout; using std::endl; int a = 0; int b = 0; void show() { cout << "a=" << a << endl << "b=" << b << endl; } struct ERR {}; void f(int x) { if (x > 100) throw ERR(); a += x; } void undo_f(int x) { a -= x; } void g(int x) { if (x > 10) throw ERR(); b += x; } int main(int ac, char **av) { int rc = 1; int x = av[1] ? atoi(av[1]) : 0; try { f(x); try { g(x); rc = 0; } catch (ERR&) { undo_f(x); } } catch (ERR&) { } show(); cout << "rc=" << rc << endl; return rc; }tryブロックの入れ子で実装している。gotoパターンに比べて、正常系の処理とエラーリカバリの処理とが 構文上のブロックとして関連づけられるのでミスが減るが、外のtryブロックほどcatchブロックが離れてしまうので コード上の見通しがわるくなる。
おなじく例外処理をつかう別の例
#include <iostream> #include <stdexcept> #include <cstdlib> using std::cout; using std::endl; using std::exception; using std::runtime_error; int a = 0; int b = 0; void show() { cout << "a=" << a << endl << "b=" << b << endl; } struct ERR {}; void f(int x) { if (x > 100) throw ERR(); a += x; } void undo_f(int x) { a -= x; } void g(int x) { if (x > 10) throw ERR(); b += x; } void proc2(int x) { try { g(x); } catch (ERR&) { throw; } } void proc1(int x) { f(x); try { proc2(x); } catch (ERR&) { undo_f(x); throw; } } int main(int ac, char **av) { int rc = 1; int x = av[1] ? atoi(av[1]) : 0; try { proc1(x); rc = 0; } catch (ERR&) { } show(); cout << "rc=" << rc << endl; return rc; }1関数1処理に限定するように書くことで構文上のtryの入れ子を避けている。 呼び出し関係から実際にはtryは入れ子になっている。
C++0xで入るlambdaをつかった例
#include <iostream> #include <stdexcept> #include <cstdlib> using std::cout; using std::endl; using std::exception; using std::runtime_error; int a = 0; int b = 0; void show() { cout << "a=" << a << endl << "b=" << b << endl; } struct ERR {}; void f(int x) { if (x > 100) throw ERR(); a += x; } void undo_f(int x) { a -= x; } void g(int x) { if (x > 10) throw ERR(); b += x; } template <typename F> struct cmd { F* pf; cmd(F& f) : pf(&f) {} ~cmd() { if (pf) (*pf)(); } void clear() { pf = 0; } }; template <typename F> cmd<F> make_cmd(F f) { return cmd<F>(f); } int main(int ac, char **av) { int rc = 1; int x = av[1] ? atoi(av[1]) : 0; try { f(x); auto uf (make_cmd([&x]() { undo_f(x); })); g(x); uf.clear(); //gが成功したらfのundoは不要。 rc = 0; } catch (ERR&) { } show(); cout << "rc=" << rc << endl; return rc; }