37

So I ran across this (IMHO) very nice idea of using a composite structure of a return value and an exception - Expected<T>. It overcomes many shortcomings of the traditional methods of error handling (exceptions, error codes).

See the Andrei Alexandrescu's talk (Systematic Error Handling in C++) and its slides.

The exceptions and error codes have basically the same usage scenarios with functions that return something and the ones that don't. Expected<T>, on the other hand, seems to be targeted only at functions that return values.

So, my questions are:

  • Have any of you tried Expected<T> in practice?
  • How would you apply this idiom to functions returning nothing (that is, void functions)?

Update:

I guess I should clarify my question. The Expected<void> specialization makes sense, but I'm more interested in how it would be used - the consistent usage idiom. The implementation itself is secondary (and easy).

For example, Alexandrescu gives this example (a bit edited):

string s = readline();
auto x = parseInt(s).get(); // throw on error
auto y = parseInt(s); // won’t throw
if (!y.valid()) {
    // ...
}

This code is "clean" in a way that it just flows naturally. We need the value - we get it. However, with expected<void> one would have to capture the returned variable and perform some operation on it (like .throwIfError() or something), which is not as elegant. And obviously, .get() doesn't make sense with void.

So, what would your code look like if you had another function, say toUpper(s), which modifies the string in-place and has no return value?

4

5 に答える 5

13

Cっぽい言語だけに焦点を当てている人にとっては新しいように見えるかもしれませんが、sum-typesをサポートする言語の好みを持っていた私たちにとってはそうではありません。

たとえば、Haskellでは次のようになります。

data Maybe a = Nothing | Just a

data Either a b = Left a | Right b

|読み取りまたはおよび最初の要素(Nothing、、、)が単なる「タグ」である場合。基本的に、sum-typeは単に共用体を区別するだけです。JustLeftRight

ここでは、次のExpected<T>ようになります。に似Either T Exceptionた特殊化を使用します。Expected<void>Maybe Exception

于 2013-02-17T16:52:17.893 に答える
13

期待を試してみましたか。実際には?

当然のことですが、この話を見る前から使っていました。

このイディオムを、何も返さない関数(つまり、void関数)にどのように適用しますか?

スライドに示されているフォームには、いくつかの微妙な意味があります。

  • 例外は値にバインドされます。
  • 必要に応じて例外を処理しても問題ありません。
  • 何らかの理由で値が無視された場合、例外は抑制されます。

を持っている場合、これは当てはまりません。値にexpected<void>関心がある人はいないためvoid、例外は常に無視されるためです。expected<T>アサーションと明示的なsuppressメンバー関数を使用して、Alexandrescusクラスからの読み取りを強制するのと同じように、これを強制します。デストラクタからの例外の再スローは、正当な理由で許可されていないため、アサーションを使用して実行する必要があります。

template <typename T> struct expected;

#ifdef NDEBUG // no asserts
template <> class expected<void> {
  std::exception_ptr spam;
public:
  template <typename E>
  expected(E const& e) : spam(std::make_exception_ptr(e)) {}
  expected(expected&& o) : spam(std::move(o.spam)) {}
  expected() : spam() {}

  bool valid() const { return !spam; }
  void get() const { if (!valid()) std::rethrow_exception(spam); }
  void suppress() {}
};
#else // with asserts, check if return value is checked
      // if all assertions do succeed, the other code is also correct
      // note: do NOT write "assert(expected.valid());"
template <> class expected<void> {
  std::exception_ptr spam;
  mutable std::atomic_bool read; // threadsafe
public:
  template <typename E>
  expected(E const& e) : spam(std::make_exception_ptr(e)), read(false) {}
  expected(expected&& o) : spam(std::move(o.spam)), read(o.read.load()) {}
  expected() : spam(), read(false) {}

  bool valid() const { read=true; return !spam; }
  void get() const { if (!valid()) std::rethrow_exception(spam); }
  void suppress() { read=true; }

  ~expected() { assert(read); }
};
#endif

expected<void> calculate(int i)
{
  if (!i) return std::invalid_argument("i must be non-null");
  return {};
}

int main()
{
  calculate(0).suppress(); // suppressing must be explicit
  if (!calculate(1).valid())
    return 1;
  calculate(5); // assert fails
}
于 2013-02-17T17:41:10.110 に答える
5

Matthieu M. が言ったように、これは C++ にとっては比較的新しいことですが、多くの関数型言語にとって新しいことではありません。

ここで 2 セントを追加したいと思います。私の意見では、「手続き型と機能型」のアプローチで、困難と違いの一部を見つけることができます。Expected<T>そして、この違いを説明するために Scala を使用したいと思います (私は Scala と C++ の両方に精通しており、より近い機能 (オプション) があると感じているため)。

Scala には Option[T] があり、これは Some(t) または None のいずれかです。特に、道徳的に と等価な Option[Unit] を持つことも可能Expected<void>です。

Scala では、使用パターンは非常に似ており、isDefined() と get() の 2 つの関数を中心に構築されています。ただし、「map()」関数もあります。

私は「map」を「isDefined + get」と同等の機能と考えるのが好きです:

if (opt.isDefined)
   opt.get.doSomething

になる

val res = opt.map(t => t.doSomething)

結果へのオプションの「伝播」

ここに、オプションを使用および構成するこの機能的なスタイルで、あなたの質問に対する答えがあると思います。

では、文字列をインプレースで変更し、戻り値を持たない toUpper(s) などの別の関数がある場合、コードはどのようになるでしょうか?

個人的には、文字列をその場で変更しないか、少なくとも何も返しません。Expected<T>うまく機能するには機能パターンが必要な「機能」概念と見なします。 toUpper(s) は、新しい文字列を返すか、変更後にそれ自体を返す必要があります。

auto s = toUpper(s);
s.get(); ...

または、Scala のようなマップを使用する

val finalS = toUpper(s).map(upperS => upperS.someOtherManipulation)

機能的なルートをたどりたくない場合は、 isDefined/valid を使用して、より手続き的な方法でコードを記述できます。

auto s = toUpper(s);
if (s.valid())
    ....

このルートをたどる場合 (おそらく必要があるため)、「void と unit」のポイントがあります: 歴史的に、void は型とは見なされませんでしたが、「型はありません」(void foo() は Pascal と同様に見なされていました)手順)。単位 (関数型言語で使用される) は、「計算」を意味する型としてより多く見られます。したがって、 Option[Unit] を返すことは、「オプションで何かを行った計算」と見なされるため、より理にかなっています。また、Expected<void>では、void は同様の意味を想定しています。つまり、意図したとおりに機能した場合 (例外的なケースがない場合)、ただ終了する (何も返さない) 計算です。少なくとも、IMO!

したがって、Expected または Option[Unit] を使用すると、結果を生成する計算と見なすことも、生成しない計算と見なすこともできます。それらを連鎖させると、それが難しいことがわかります。

auto c1 = doSomething(s); //do something on s, either succeed or fail
if (c1.valid()) {
   auto c2 = doSomethingElse(s); //do something on s, either succeed or fail
   if (c2.valid()) { 
        ...

あまりきれいではありません。

Map in Scala で少しきれいに

doSomething(s) //do something on s, either succeed or fail
   .map(_ => doSomethingElse(s) //do something on s, either succeed or fail
   .map(_ => ...)

どちらが優れていますが、理想にはほど遠いです。ここでは、Maybe モナドが明らかに勝っています...しかし、それは別の話です..

于 2013-02-28T10:12:34.850 に答える
2

このビデオを見て以来、私は同じ質問を考えていました。これまでのところ、期待されていることについて説得力のある議論を見つけることができませんでした. これまでのところ、次のことを思いつきました。

  • 値または例外のいずれかがあるため、Expected は適切です。スロー可能なすべての関数に try{}catch() を使用する必要はありません。したがって、戻り値を持つすべてのスロー関数に使用します
  • スローしないすべての関数は、 でマークする必要がありnoexceptます。毎日。
  • 何も返さず、noexcepttry{}catch{} でラップする必要があるとマークされていないすべての関数

これらのステートメントが成り立つ場合、実装の詳細を覗かないと、どの例外がスローされる可能性があるのか​​ わかりません。

クラス実装の内部 (プライベート メソッドの奥深くなど) に何らかの例外がある場合は、インターフェイス メソッドでそれをキャッチし、Expected を返す必要があるため、Expected はコードにいくつかのオーバーヘッドを課します。何かを返すという概念を持つメソッドにとってはかなり許容できると思いますが、設計上戻り値を持たないメソッドに混乱と混乱をもたらすと思います。その上、私にとって、何も返すはずのないものから何かを返すことは非常に不自然です。

于 2013-02-27T06:21:30.673 に答える