例外と予想:妥協点を見つけましょう

これは、今日公開したかったブログ投稿ではなく、公開しなければならなかったブログ投稿です。

Simon は、エラー処理に ADT を使用することについてブログを書き、興味深い reddit の議論につながりました.Vittorio は、この問題に関する彼の考えを共有したいと考え、さらに大きな reddit の議論につながりました.今、私は声を上げて、合理的な解決策を提供したいと思います.

リターン コードと例外という古くからの質問がありますが、今回はリターン コードがアップグレードされました:07

例外に関する問題

私が知る限り、人々は例外を除いて次の問題を抱えています:

    <リ>

    それらは暗黙的です:一部のコードを見ると、例外をスローできるものとできないものがあることは明らかではありません。これにより、正確性を判断するのが難しくなります。 ただし、これは関数シグネチャでのみ表示され、何もスローしないすべての関数が 22 とマークされているわけではありません 、そのため、関数のドキュメントを参照する必要があります。

    <リ>

    それらを正しく使用することは困難です:例外セーフ コードを記述することは困難です。特に、扱っている型がわからない一般的なコードでは困難です。 、または型に追加の要件を設定する必要があります (つまり、この関数はスローしてはなりません) が、それらを静的に検証することは現在不可能です。

  • それらは簡単に構成できません:現在の例外は 1 つだけです。複数の例外を持つことはできません。これは、たとえば C++17 並列アルゴリズムで問題でした。複数の例外がスローされた場合ワーカースレッド?それらすべてを呼び出し元に報告するにはどうすればよいでしょうか?実装はその問題を解決することを断念し、例外がスローされた場合にプログラムを終了することにしました。

ADT の問題

37 のような型 は、関数の世界で代数データ型として知られているものです。実際、これは一般的なパターンです。

エラー処理用の ADT には次の不満があります:

    <リ>

    それらは明示的です:ADT を返す関数がある場合、それを呼び出すすべての関数がエラーを処理する必要があります。それらは単に渡されるだけではなく、余分な作業を行う必要があります。

    <リ>

    それらは人間工学的ではありません:複数のことを順番に実行したい場合は、冗長なコードを記述するか、関数型パラダイムを使用する必要がありますが、これは C++ での使用には特に適していません。与えられた 2 つの例を比較してください:

return crop_to_cat(img)
       .and_then(add_bow_tie)
       .and_then(make_eyes_sparkle)
       .map(make_smaller)
       .map(add_rainbow);

// vs.

crop_to_cat(img);
add_bow_tie(img);
make_eyes_sparkle(img);
make_smaller(img);
add_rainbow(img);
  • これらは無視できます:これらは単なる戻り値の型であり、簡単に無視できます。 Walter Bright の言葉を引用すると、「42 の戻り値をチェックする人は何人ですか。 ?

全体像を見る

ほとんどのものと同様に、欠点はその反対です。「例外が暗黙的すぎる!」 — 「ADT は露骨すぎる!」

それでは、一歩下がって全体像を見てみましょう。特に:ライブラリを作成していて、失敗する可能性のある関数がある場合、エラーをどのように報告しますか?

Tony van Eerd からのこの回答を引用します。

真に柔軟な API を作成する場合は、例外とエラー リターン コードの両方を作成する必要があります。エラーが「例外的」である場合とそうでない場合があるためです。

これは、C++17 ファイルシステム ライブラリが行ったことです:

void copy(const path& from, const path& to); // throws an exception on error
void copy(const path& from, const path& to, error_code& ec); // sets error code

ただし、これにより、一方を他方の観点から実装するときに発生する、コードの重複またはボイラープレートが発生します。

それで、あなたは何をすることになっていますか?

他の人がしていることを行います。

特に、さまざまなプログラミング言語に目を向けてください。新しいプログラミング言語について聞くとき、私は次の 2 つのことを調べます。

<オール> <リ>

ジェネリック プログラミングはどのように機能しますか?

<リ>

エラー処理はどのように機能しますか?

どちらも比較的難しい問題であり、C++ の方法に限定されていない場合、どのように解決できるかを見るのは興味深いことです。そこで、他の 2 つのプログラミング言語、Swift と Rust がどのようにエラー処理を解決するかを見てみましょう。

Swift でのエラー処理

Swift は例外の使用を選択します。

ただし、上記の問題はありません (少なくとも C++ ほどではありません)。

    <リ>

    それらは明示的です:Swiftでは、例外をスローする可能性のある関数がある場合、関数を 54 として指定する必要があります :

    func canThrowErrors() throws -> String
    
    func cannotThrowErrors() -> String
    

    しかし 67 とは異なります 、これは静的に適用されます。

    さらに、呼び出すとき 例外をスローする可能性のある関数、それも明確にする必要があります:

    result = try canThrowErrors();
    result2 = cannotThrowErrors();
    

    これにより、例外をスローできる関数とスローできない関数がすぐにわかります。

    <リ>

    それらを正しく使用することは難しくありません:もちろん、例外の安全性について心配する必要はありますが、コードに暗黙の要件はありません:それらは明確にされています。
    73 のように 型システムの一部であり、Swift プロトコル (基本的には C++0x の概念) も同様に処理します。特定のプロトコルがスロー関数を提供することを許可しない場合は、スロー関数を持つ型を渡すことはできません。また、86 RAII のボイラープレートなしで確実にクリーンアップできます。

    <リ>

    それらは (ある程度) 構成可能です:94 で関数を呼び出すことに加えて 、 105 で呼び出すこともできます :これにより、オプションに変換され、構成可能になります。110 もあります。 呼び出しが例外をスローした場合、プログラムを終了します。

Rust でのエラー処理

一方、Rust はエラー処理に ADT を使用することにしました。特に、127 — いずれかの結果値 137 またはエラー 146 — が使用されます。

また、私がリストした問題のほとんどを解決することもできました:

    <リ>

    それらは人間工学的です:ADT を扱うときの一般的なパターンは次のとおりです:

    result = foo();
    if (!result)
      return result.error();
    // do something with result.value()
    

    このパターンは非常に一般的であるため、Rust はボイラープレート ソリューションを提供しています:

    // old way
    result = try!(foo());
    
    // new built-in language feature
    result = foo()?;
    

    これは上記のコードと同じことを行います:エラーで早期に戻るか、それ以外の場合は続行します。

    さらに、Rust は関数スタイル機能と適切なパターン マッチングも提供します。

    <リ>

    無視してはいけません:159 特別な属性でマークされているため、戻り値が単に破棄された場合、コンパイラは文句を言います.

両方の世界を組み合わせる

ここで興味深いのは、Swift と Rust の両方のエラー処理が非常に似ていることです。主な違いは、エラーが呼び出しスタックを介して転送される方法です。

これは、両方のアプローチが特定の状況に対して優れたソリューションであることを意味します。例外がスローされた場合でも実行時のオーバーヘッドがあるため、例外ではない場合には使用しないでください。まれなエラーには使用しないでください。

ただし、広く使用可能なライブラリを作成している場合、呼び出し元だけが状況が例外的ではないかどうかを知っています!

したがって、エラーを報告する方法が必要です:

  • 暗黙的ですが、完全に隠されているわけではありません
  • 明示的ですが、冗長すぎません
  • あらゆる状況で使用できる柔軟性
  • 完全に型システムの一部であるため、コンセプトでチェックできます
  • 無視することはできません

完全に型システムの一部であるものが必要な場合は、今すぐ 、言語を変更せずに、エラー情報を戻り値の型に入れる必要があります。

ただし、これには追加の利点があります。戻り値を例外に変換することは、オーバーヘッドなしで実行できます。唯一のコストは、169 の追加の分岐です。 、しかし、結果を生成する関数にはおそらく既に分岐があります.関数の呼び出しがインライン化されている場合、余分な分岐を削除できます.

そこで、新しい戻り値の型が必要です:173 としましょう .187 によく似ています または Rust の 192 「通常の」戻り値 203 を含む またはいくつかのエラー情報 212 .そして 224 とは異なります オプションのようなインターフェースだけでなく、モナドのエラー処理ルーチン (235248 etc).機能的な ADT を使用したい人は、すでに満足しています.

例外ファンを喜ばせるために、関数 252 も提供しましょう 262 を返します または 274 を変換します 例外を使用して失敗を処理する場合は、関数の後に数文字を入力するだけで済みます。

失敗がプログラミングのミスである場合は、286 を呼び出してください。 チェックなし。エラーが発生した場合、デバッグ アサーションが発生する可能性があります。

しかし、エラーが無視されたらどうなるでしょうか?

C++17 が 295 を追加 、これは素晴らしいですが、簡単に抑制できます。私は 301 のようなものを提案します 314 のデストラクタで エラーを処理せずに結果を破棄すると、プログラムが終了します。そのように、処理を忘れたり、327 のような明示的なものを呼び出したりしてはなりません。 .

これにより、プログラム内で単一のライブラリ関数を呼び出す場合の問題はすべて解決されます。ただし、複数の結果を構成したり、汎用コードを記述したりする必要があるライブラリの問題は解決されません。331 の処理 例外よりもさらに冗長です:

result<T, E> calculate_bar()
{
  auto first_result = calculate_foo1();
  if (!first_result)
    return first_result.error();

  auto second_result = calculate_foo2(first_result.value());
  if (!second_result)
    return second_result.error();

  return bar(second_result.value());
}

ただし、これは小さな言語の追加で解決できます - operator 345 .Rust の 354 です または 362 そしてそれを完璧にします:

result<T, E> calculate_bar()
{
  auto first_result = try calculate_foo1();
  auto second_result = try calculate_foo2(first_result);
  return bar(second_result);
}

結論

エラー処理が難しい。

しかし、私は 378 のようなものだと本当に思います 何らかの形式の 380 と組み合わせて説明しました operator は問題の解決策になる可能性があります。

もちろん、多くの詳細と重要な設計上の決定については触れていません:

    <リ>

    390 とは まさに?すべての関数で同じにする必要がありますか?一方では、これにより 409 を返すすべての関数として構成が簡単になります。 戻り値の型は同じです。しかし、これは融通が利かないのではないでしょうか?

    <リ>

    410 はいつ、どのように 例外に変換されますか?そして、どの例外タイプですか?

    <リ>

この 427 にはさまざまな実装があります この理由で:提案された 436 (Boost.)結果は別のものです。それらを参照することをお勧めします。著者は、私よりも多くの時間を問題について考えることに費やしています.

もちろん、単純にアプリケーション コードを記述する場合は、好きな方を使用できます。ただし、汎用ライブラリを記述する場合は、これらの手法の採用を検討してください。

このエラー処理方法は、すべての種類のエラーに使用できるわけではないことに注意してください。たとえば、メモリ不足です。その場合は、ここで説明した例外ハンドラー手法を使用する必要があります。