右上➚

プログラミングに関するメモをのこしていきます

C++ で result 型を作る

Haskell や Rust など多くの強力な型システムを持つプログラミング言語は、Either とか Result といった「失敗するかもしれない」計算の値を示す型を持っています。

現在の C++ の標準ライブラリにはこのような型はありませんので、それを自作してみようというわけです。

ちなみに現在策定中のC++17には std::optional が入ることが決定しているようです。これは、result と同様に失敗するかもしれな値を示しますが、失敗した原因がなんなのかを通知する仕組みを持っていません。

そもそもどういう型か

Rust の Result 型を例にみてみます。

enum Result<V, E> {
  Ok(V),
  Err(E),
}

Rust における Result 型はだいたいこんな感じで定義されています。

Result は型引数を2つとります。V が成功時の値で、E が失敗時のエラー情報です。 例えば、fn parse_int(s: &str) -> Result<isize, String>; は、文字列を受け取り、それが整数としてパース出来れば isize に変換し、Ok(isize) として返します。 もし整数としてパース出来ないなどのエラーがあれば、それを String で表現し、Err(String) で返します。

本質的にはこれが全てです。ここに、Result から中身を取り出す(Err なら panic する)関数などを定義してあげれば便利にエラー状態を表現できます。
(Rust の try マクロはとても便利ですよね)

まずはベースとなる result を作る

まずはベースとなる result 型を作ってみます。

template <typename T, typename E>
struct result {

  result(T const& ok) : t(tag::OK) {
    ok_ = ok;
  }

  result(E const& e) : t(tag::OK) {
    err_ = e;
  }

  ~result() {
    switch (t) {
      case tag::OK:
        ok_.~T();
        break;
      case tag::ERROR:
        err_.~E();
        break;
    }
  }

  result(result const& r): t(r.t) {
    switch (t) {
      case tag::OK:
        ok_ = r.ok_;
        break;
      case tag::ERROR:
        err_ = r.err_;
        break;
    }
  }

  T const& get() const {
    if (t != tag::OK) {
      throw "invalid get operation";
    }
    return ok_;
  }

  E const& get_error() const {
    if (t != tag::ERROR) {
      throw "invalid get operation";
    }
    return err_;
  }

private:
  enum class tag {
    OK,
    ERROR,
  };
  tag t;
  union {
    T ok_;
    E err_;
  };

};

かなり雑ですが、ざっくりこんな感じになるはずです。 C++11 から拡張されて自由度がかなり高くなった union がとても便利です。

これで result<int, std::string>(1).get() とやれば 1 が返るし result<int, std::string>(std::string("test")).get_error()"test" が返るはずです。

C++ でやると何が難しいか

C++ で難しいのは、Rustより弱い型推論が引き起こす問題です。
Rust では、Ok(1isize) とか Err("error!".to_owned()) とすれば、その値がどういう型であることが期待されているのかまで含めて型推論や単一化が行われます。 すなわち、Ok(1isize) だけを見てもエラーの型がわからないため、Result<isize, E>E を決定することが出来ないが、Rust は強力な型推論機構を持つため、これを決定することが出来ます。

一方、C++ では result<int, std::string> f() { return 1; }int から result<int, std::string> の暗黙変換がきくので可能ですが、result<int, int> などとした瞬間、暗黙変換に頼ることはできなくなります。 そこで、出来れば ok(1) とか err("test") という感じにしたいのですが、これは一筋縄では行きません。

template <typename T, typename E> 
result<T, E> ok(T);

これだと T は推論されても E が推論されないので、ok<int, std::string>(1) などとしなければなりません。これは使いづらすぎます。

じゃあどうするか

先ほどとは違う形ですが、やっぱり暗黙の型変換を応用します。

要するに ok を表す型と error を表す型を区別しつつ、result<V, E> とはなるべくシームレスに変換をしたいというわけですから、それぞれ専用の型を作ってしまえば良いのです。

template <typename T>
struct ok_value {
  explicit ok_value(T t): t(t) {}

  template <typename V, typename E>
  operator result<V, E> () const;

private:
  T t;
};

template <typename T>
template <typename V, typename E>
ok_value<T>::operator result<V, E> () const {
  return result<V, E>(t);
}

template <typename T>
ok_value<T> ok(T t) {
  return ok_value<T>(t);
}

ok 側だけ示しました。
ok 関数はテンプレートになっており、T 型の値をとって ok_value<T> を返します。(本当は値渡し以外にも対応すべきですが、簡単のために値渡しだけ実装しています)

ok_value<T> は型変換演算子 operator result<V, E>() const を持ちます。これによって ok_value から result への暗黙変換が可能になります。

ok_value<T>result<T, E> に変換出来れば良さそうに見えるのですが、それでは不十分です。
ok("test")ok_value<const char*> を返します。ok_value<T> -> result<T, E> の変換しか提供していない場合は、result<std::string, E> への変換ができなくなってしまいます。これは不便ですよね。
そこで新たにテンプレート引数を導入することでこれを解決しています。もっときちんとやるなら std::is_constructible などを使ってチェックをするべきだとは思いますが。

error 側もほぼ同様のコードを書いてやれば、

result<int, std::string> parse_digit(char c) {
  if (c < '0' || '9' < c) {
    return error("invalid character");
  }
  return ok(c - '0');
}

というように書けます。

まとめ

T から result<T, E> への暗黙変換を許すという方針も全然ありだとは思いますが、個人的に Rust などで ok なら ok と明示するスタイルに慣れているので、こっちのほうが気に入っています。
明らかに正常に値を返していそうな感じがコードにあらわれて好きです。

暗黙の型変換って危険だしあまり良いイメージはないと思うのですが、やっぱりあれば便利ですね。 C++ を使っている時点で気を抜いたら死なので、「取り扱いを把握して全力で注意しながら使えば危険じゃない」という気持ちで便利に使いたいものです。