先日、完了したら完了通知を呼び出すような処理を書いていて、(自分で仕込んだ)バグにはまったので書いてみます。
困った問題
ものすごく簡単に書くと、
typedef boost::function< void() > handler_t;
// 終わったらhandlerを呼ぶ
void func(int x, handler_t handler)
{
// some logic
// some logic
// some logic
handler();
}
こんなロジックがありましたが、うっかりこんな変更を加えてしまい、
typedef boost::function< void() > handler_t;
void func(int x, handler_t handler)
{
if (!x) return;
// some logic
// some logic
// some logic
handler();
}
handler が呼び出されなくなってしまいました。
ここで、テストが足りなかった、今度から気をつけよう、と思うだけでは進歩がないと思います。
ちょっとreturn文を追加する、こんな些細なことで構造が壊れてしまうような設計の脆弱性に問題があったと考えるわけです。
気をつけなくても、必ずhandler()が呼び出されるようにするにはどうすればいいのか、考えてみました。
解決案
案1 関数を分ける
非常につまらない話なのですが、単に内部ロジックを別の関数をわけておけば、こういう間違いは犯さなかったと思います。
typedef boost::function< void() > handler_t;
void func_impl(int x)
{
if (!x) return;
// some logic
// some logic
// some logic
}
void func(int x, handler_t handler)
{
func_impl(x);
handler();
}
案2 Scope.Exit 的なライブラリーを使う
BOOST_SCOPE_EXITを知らないとコードが読めないのは欠点ですが、慣れるとこっちのほうが便利で安全な感じがしますね。
#include <boost/scope_exit.hpp>
typedef boost::function< void() > handler_t;
void func(int x, handler_t handler)
{
BOOST_SCOPE_EXIT(void) {
handler();
} BOOST_SCOPE_EXIT_END
if (!x) return;
// some logic
// some logic
// some logic
}
さらに考えると
元のソースと、案2のソースは例外が発生した場合に、handler()が呼び出されるかどうかが違うので、挙動が変わっていますね。
エラーハンドリングのポリシーも考える必要がありそうです。