yohhoyの日記

技術的メモをしていきたい日記

C++コルーチン送出例外のハンドリング戦略

C++20コルーチンからの例外送出ハンドリングに関するメモ。C++コルーチンライブラリの設計者向け。

C++コルーチン言語仕様では、コルーチン送出例外ハンドリングのカスタマイズポイントとしてpromise_type::unhandled_exception関数を規定する。プログラマが記述したコルーチン本体処理に対して、C++コンパイラは下記のようなコード展開を行う(一部は簡略化)。

/* コルーチンの展開後コード */ {
  promise_type _promise;
  try {
    co_await _promise.initial_suspend();
    /* コルーチン本体処理 */
  } catch (...) {
    _promise.unhandled_exception();  // ★
  }
  co_await _promise.final_suspend();
}

C++コールチンライブラリ設計者の選択肢としては、3種類の例外ハンドリング戦略が考えられる。

  • [A] コルーチンの呼出元(caller)/再開元(resumer)にそのまま例外伝搬させる。ジェネレータ(generator)などの同期型コルーチン向け。
  • [B] コルーチンの戻り値型オブジェクトに例外保持しておき、値の読み取りタイミングで例外再スローする。並行タスクや非同期I/Oなどの非同期コルーチン向け。
  • [C] 例外送出=プログラム異常終了(std::terminate)。

それぞれの例外ハンドリングにおけるpromise_type::unhandled_exception実装イメージは下記の通り。

// [A] 再スロー
void unhandled_exception()
{
  throw;  // 現在の例外を再スロー
  // コルーチン処理と呼出元は同一スレッドの前提。
  // コルーチンの呼出元(caller)/再開元(resumer)に
  // コルーチン内部から送出された例外が伝搬する。
}

// [B] 例外保持
void unhandled_exception()
{
  ep_ = std::current_exception();
  // コルーチン処理と値の取出操作は別スレッドの可能性あり。
  // メンバ変数 std::exception_ptr ep_; に格納しておき、
  // コルーチン計算結果の取出操作メンバ関数の実装にて
  // 値を返す代わりに std::rethrow_exception(ep_); を実行。
}

// [C] プログラム停止
void unhandled_exception()
{
  std::terminate();
}

選択肢[A]の動作は、C++20統合前のC++コルーチン拡張Coroutine TSに対するP0664R6 Issue#25にて明言されている。

25. Allow unhandled exception escape the user-defined body of the coroutine and give it well defined semantics

The proposed resolution is to eliminate the undefined behavior in the following manner:

  • Allow an exception to escape p.unhandled_exception() and, in that case, consider the coroutine to be at the final suspend point. Reminder: when a coroutine is at the final suspend point, the coroutine can only be destroyed and a call to member function done() of the coroutine handle associated with that coroutine returns true.
  • Eliminate possibility of an exception being thrown from evaluation of an expression co_await p.final_suspend() by stating that final_suspend member function of the coroutine promise and await_resume, await_ready, and await_suspend members of the object returned from final_suspend shall have non-throwing exception specification.

This resolution allows generator implementations to define unhandled_exception as follows:

void unhandled_exception() { throw; } 

With this implementation, if a user of the generator pulls the next value, and during computation of the next value an exception will occur in the user authored body it will be propagate back to the user and the coroutine will be put into a final suspend state and ready to be destroyed when generator destructors is run.

P0664R6 C++ Coroutine TS Issues

関連URL