読者です 読者をやめる 読者になる 読者になる

エラー値と正常値を表す汎用的な型:expected

C++

Andrei Alexandrescuが考案したExpected<T>というクラスを、いまBoostとC++標準に提案する動きがあります。

Vicente Botet EscribaさんとPierre Talbotさんが現在開発を進めているexpectedクラスは、エラー値と正常値を汎用的に表す型です。boost::optionalboost::variantの亜種で、HaskellScalaにある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を入れてもいいです。

よく考えられた、とても使いやすいクラスになってますね。

参照