2014年5月23日金曜日

C++: destruct_invoker_t スコープアウトに伴う遅延実行を実現する1つの方策とその例外処理について

「ある処理Aをしたならば、別のある処理Bをスコープアウトに実行予約したい」、そんなニーズが発生する事があります。

その際に安全な方法としてはユーザーにAとBの間にしたい処理をラムダ式にラップして渡して貰えるように上手くラップした関数を作るのが良いかと思います。

これは a() や b() や user_proc() で例外eが送出されるとしても、fの外部にもそのeはfを容易した開発者にも、それを利用するユーザーにも、期待通りに伝搬されて、ユーザーがtry-catchを適切に記述していればうまく例外処理を行えます。

この方法のメリットは手軽で安全な事ですが、デメリットとして user_proc() では何をされても a() と b() の間でその user_proc として渡されたファンクターの通りにしか評価できない事です。a()とb()の間でユーザーに一見任意にコードを実行させつつ、実際にはa()の後に、ユーザーのコードの一部をバッファリングして実行をb()の直前まで遅延したり、特別な手順で実行させたり、そうしてから b() を呼びたい場合に対応しにくくなります。また、ユーザーにはコード記述に際してファンクターを関数に投げるという平たくコードを書くよりはC系の基本的には手続き型で記述される事の多い言語としては少しだけ特別な記述方法を強いる事になります。また、このような仕組みが重複する場合や複数の a1(),a2(),a3()とb1(),b2(),b3()の組み合わせの実現などに際してコード量も相応に増えてしまいます。

そこで、 destruct_invoker_t を定義してみます。これはRAII風に処理Aと処理Bをリソースの初期化と開放に対応付けて見立てて、コンストラクターでBをファンクターとして受け取って保持しておいて、スコープアウトに伴ってBがデストラクターによって発動するパターンの実装です。

パターンの最低限の手抜き実装です。しかしこれはデストラクターでの例外発生を考慮していないので、与えられた遅延する処理Bの中で例外が発生する場合にあまり良くない事が起こります。
terminate called without an active exception
とか。 destruct_invoker_t を使うコードの外側でいくら try-catch を適切に書いてあったとしても、 destruct_invoker_t で 処理Bが実行されるタイミングが destruct_invoker_t::~destruct_invoker_t() の内部となるので、デストラクター内で発生した例外についての例外伝搬の特例により、デストラクター内でこの例外がcatchされない場合には直ちに std::terminate() がコールされてしまいます。すると、この destruct_invoker_t の外側には例外が伝搬されないので、これを使う外側で例外処理を定義しても意味がない状態に陥ってしまいます。

そこで、 destruct_invoker_t のパターンを使う場合のせめてもの設計指針として、処理Bに伴う例外発生のtry-catch戦略として例外ポインターのセットによる例外の遅延を施します。

こんな具合に。

するとこれは処理Bで例外が発生してもとりあえずは警告を発しつつもその例外の評価は static std::exception_ptr _pe 例外ポインターに std::current_exception() を与えて遅延し、この機能を使った外側で static destruct_invoker_t::rethrow_if_deferred_exception() するか、或いは次回この destruct_invoker_t を使おうとした際にコンストラクターでそれを自動的に行い遅延された例外を検出した場合には std::rethrow_exception( _pe ); により例外をリスローし、destruct_invoker_t の外に定義する try-catch でこれを捉えられるようになります。

destruct_invoker_t のパターンを使う場合にはメリットは様々ありますが、最大の問題としてこの例外対策が不十分になってしまう事が挙げられます。

もし、これを直接使うのがライブラリーの内部だけで、必要十分に遅延実行を予約された例外がリスローされるようにできるのならば、それを暗黙的にユーザーに提供し、 auto scoped_hoge = some_library -> scoped_hoge(); のように invoker を使わせても凡そ安全でしょう。( auto some_library_t::scoped_hoge() -> destruct_invoker_t; )しかし、もしもユーザーがライブラリー開発者の意図しないところで手作業で destruct_invoker_t を不用意に用いてしまった場合には、適切なリスローを保証できません。せいぜいドキュメントで万が一 destruct_invoker_t をユーザーコードで直接つかいたい場合に対する注意喚起を施しておく程度です。あげくにそれがWindowsでコンパイルされた非コマンドラインアプリ向けのアプリだと、それでは通常ユーザーはコマンドラインから実行せずに、結果エラー出力が出ませんから、ユーザーも例外処理の遅延実行予約についてせめてものstd::cerrへの警告も見逃してしまう可能性があります。

destruct_invoker_t のパターンも例外処理とその仕様を踏まえて使われるのなら、便利なのですけれど、例外について絶対にリスローされる保証を付けられない不安はやはり残ります。

0 件のコメント:

コメントを投稿