Andrei Alexandrescuが考案したExpected<T>
というクラスを、いまBoostとC++標準に提案する動きがあります。
Vicente Botet EscribaさんとPierre Talbotさんが現在開発を進めているexpected
クラスは、エラー値と正常値を汎用的に表す型です。boost::optional
やboost::variant
の亜種で、HaskellやScalaにあるEitherをエラーに特化させたものです。Haskell的にはMonadErrorに相当するそうです。私も以前、似たようなのを書きました:「エラー許容型を作った」。
ゼロ割りを適切にハンドリングする、安全な割り算関数に例外を使うと、以下のようなコードになります:
struct DivideByZero: public std::exception { … }; double safe_divide(double i, double j) { if (j == 0) throw DivideByZero(); else return i / j; }
expected
クラスを使う場合は、以下のようになります:
enum class arithmetic_errc { divide_by_zero, // 9/0 == ? not_integer_division // 5/2 == 2.5 (which is not an integer) }; expected<error_condition, double> safe_divide(double i, double j) { if (j == 0) return make_unexpected(arithmetic_errc::divide_by_zero); else return i / j; }
expected
はテンプレート引数として、エラーを表す型と、正常値を表す型をとります。エラー値を持つexpected
オブジェクトを作るには、make_unexpected()
関数を使用します。
正常値を取り出す方法はいくつかあり、まずoperator bool()
とvalue()
メンバ関数を使うという、optional
と同じアプローチのものがあります:
if (auto result = safe_divide(a, b)) { // 正常値が入っているかを判定 double x = result.value(); // 正常値を取り出す } else { error_condition x = result.error(); // エラー値を取り出す }
このほかに、モナド的なアプローチとして、map()
メンバ関数を使うものもあります。これは、エラーになるかもしれない処理を連鎖させるときに、とくに有効です。
expected<error_condition, double> f1(double i, double j, double k) { return safe_divide(j, k).map([&](double q) { return i + q; // safe_divide()の結果が正常値だったらこの関数が呼ばれ、 // エラー値だったらそれがそのまま返る。 }); }
エラー値をハンドリングしたい場合は、catch_error()
メンバ関数を使います。
expected<error_condition, double> f1(double i, double j, double k) { return safe_divide(j, k).catch_error([](error_condition e) { std::cout << e.message() << std::endl; }); }
また、エラーの型はなんでも入れられるので、エラーを表す文字列を入れてもいいですし、exception_ptr
を入れてもいいです。
よく考えられた、とても使いやすいクラスになってますね。