「C ++ of the future」の確定的な例外とエラー処理



Habrtでは、「ゼロオーバーヘッドの決定論的例外」と呼ばれるC ++標準の魅力的な提案について言及されていないのは奇妙です。 この迷惑な省略を修正します。







例外のオーバーヘッドが心配な場合、または例外サポートなしでコードをコンパイルする必要がある場合、またはC ++ 2bのエラー処理( 最近の投稿への参照)で何が起こるのか疑問に思っている場合は、catに問い合わせてください。 あなたは今トピックで見つけることができるすべてのものの絞り出しと、いくつかの世論調査を待っています。







以下の説明は、静的な例外だけでなく、標準に関連する提案、およびその他のあらゆる種類のエラー処理方法についても行われます。 構文を見るためにここに行った場合は、次のとおりです。







double safe_divide(int x, int y) throws(arithmetic_error) { if (y == 0) { throw arithmetic_error::divide_by_zero; } else { return as_double(x) / y; } } void caller() noexcept { try { cout << safe_divide(5, 2); } catch (arithmetic_error e) { cout << e; } }
      
      





特定のタイプのエラーが重要でない/不明な場合は、 throws



and catch (std::error e)



だけthrows



使用できます。







知っておきたい



std::optional



およびstd::expected





関数で発生する可能性のあるエラーは、例外をスローするほど致命的ではないと判断しましょう。 従来、エラー情報はoutパラメーターを使用して返されます。 たとえば、 Filesystem TSは同様の機能を多数提供しています。







 uintmax_t file_size(const path& p, error_code& ec);
      
      





(ファイルが見つからなかったため、例外をスローしませんか?)ただし、エラーコードの処理は煩雑でバグが発生しやすいです。 エラーコードは確認するのを忘れがちです。 最新のコードスタイルは、出力パラメーター使用が禁止されているため、結果全体を含む構造体を返すことをお勧めします。







しばらくの間、Boostは、このような「致命的でない」エラーを処理するエレガントなソリューションを提供してきました。特定のシナリオでは、正しいプログラムで何百人も発生する可能性があります。







 expected<uintmax_t, error_code> file_size(const path& p);
      
      





expected



タイプはvariant



と似ていますが、「result」と「error」を操作するための便利なインターフェースを提供します。 デフォルトでは、 expected



結果はexpected



保存さexpected



ます。 file_size



実装は次のようになります。







 file_info* info = read_file_info(p); if (info != null) { uintmax_t size = info->size; return size; // <== } else { error_code error = get_error(); return std::unexpected(error); // <== }
      
      





エラーの原因が私たちにとって興味のないものである場合、またはエラーが結果の「不在」のみで構成されるoptional



は、 optional



を使用できます。







 optional<int> parse_int(const std::string& s); optional<U> get_or_null(map<T, U> m, const T& key);
      
      





BoostのC ++ 17では、 optionalがstdに追加されました( optional<T&>



サポートなし)。 C ++ 20では、 期待どおりに追加される場合があります(これは単なる提案であり、 RamzesXIの修正に感謝します)。







契約



コントラクト (概念と混同しないでください)は、関数パラメーターに制限を課す新しい方法であり、C ++ 20で追加されました。 追加された3つの注釈:









 double unsafe_at(vector<T> v, size_t i) [[expects: i < v.size()]]; double sqrt(double x) [[expects: x >= 0]] [[ensures ret: ret >= 0]]; value fetch_single(key e) { vector<value> result = fetch(vector<key>{e}); [[assert result.size() == 1]]; return v[0]; }
      
      





契約違反に対して設定できます:









コンパイラは関数コードを最適化するために契約からの保証を使用するため、契約違反後にプログラム操作を継続することは不可能です。 契約が履行されるというわずかな疑いがある場合は、追加のチェックを追加する価値があります。







std :: error_code



C ++ 11で追加された<system_error>



ライブラリを使用すると、プログラム内のエラーコードの処理を統一できます。 std :: error_codeは、 int



型のエラーコードと、子孫クラスstd :: error_categoryのオブジェクトへのポインターで構成されます。 実際、このオブジェクトは仮想関数のテーブルの役割を果たし、指定されたstd::error_code



動作を決定します。







std::error_code



を作成するには、 std::error_category



下位std::error_category



を定義し、仮想メソッドを実装する必要があります。最も重要なものは次のとおりです。







 virtual std::string message(int c) const = 0;
      
      





std::error_category



グローバル変数も作成する必要があります。 error_code + expectedを使用したエラー処理は次のようになります。







 template <typename T> using result = expected<T, std::error_code>; my::file_handle open_internal(const std::string& name, int& error); auto open_file(const std::string& name) -> result<my::file> { int raw_error = 0; my::file_handle maybe_result = open_internal(name, &raw_error); std::error_code error{raw_error, my::filesystem_error}; if (error) { return unexpected{error}; } else { return my::file{maybe_result}; } }
      
      





std::error_code



値0はエラーがないことを意味することが重要です。 これがエラーコードに当てはまらない場合は、システムエラーコードをstd::error_code



に変換する前に、コード0をSUCCESSに、またはその逆に置き換える必要があります。







すべてのシステムエラーコードは、 errcおよびsystem_categoryで説明されています。 特定の段階でエラーコードの手動転送が面倒になった場合、 std::system_error



でエラーコードをいつでもラップしてstd::system_error









破壊的な動き/簡単に再配置可能



いくつかのリソースを所有するオブジェクトの別のクラスを作成する必要があります。 ほとんどの場合、移動不可能なオブジェクト(C ++ 17より前は関数から返せないオブジェクト)を操作するのは不便であるため、コピー不可で移動可能にしたいと思うでしょう。







しかし、問題は次のとおりです。いずれにしても、移動したオブジェクトを削除する必要があります。 したがって、特別な「移動元」状態、つまり何も削除しない「空の」オブジェクトが必要です。 各C ++クラスは空の状態である必要があります。つまり、コンストラクターからデストラクターまで、正確性の不変(保証)を持つクラスを作成することはできません。 たとえば、その存続期間を通して開いているファイルの正しいopen_file



クラスを作成することは不可能です。 RAIIを積極的に使用する数少ない言語の1つでこれを観察するのは奇妙です。







別の問題は、移動時の古いオブジェクトのゼロ化がオーバーヘッドを追加することです:移動時の古いポインターのゼロ化のヒープにより、 std::vector<std::unique_ptr<T>>



std::vector<T*>



最大2倍遅くすることができます、ダミーの削除が続きます。







C ++開発者は長い間、Rustをなめていました。Rustでは、再配置されたオブジェクトでデストラクタが呼び出されません。 この機能は破壊的移動と呼ばれます。 残念ながら、Proposal Trivially relocatableでは、C ++に追加することはできません。 しかし、オーバーヘッドの問題は解決されます。







古いオブジェクトの移動と削除の2つの操作が、古いオブジェクトから新しいオブジェクトへのmemcpyと同等である場合、クラスは自明に再配置可能と見なされます。 古いオブジェクトは削除されず、作成者は「床に落とす」と呼びます。







次の(再帰)条件のいずれかが真である場合、型はコンパイラの観点から自明に再配置可能です。







  1. 簡単に移動可能+簡単に破壊可能( int



    またはPOD構造など)
  2. これは、 [[trivially_relocatable]]



    属性でマークされたクラスです[[trivially_relocatable]]



  3. これは、すべてのメンバーが自明に再配置可能なクラスです。


この情報はstd::uninitialized_relocate



で使用できます。これは、通常の方法でmove init + deleteを実行するか、可能であれば加速します。 std::string



std::vector



std::unique_ptr



など、ほとんどのタイプの標準ライブラリを[[trivially_relocatable]]



としてマークすることをお勧めします。 オーバーヘッドstd::vector<std::unique_ptr<T>>



これを念頭に置いて提案は消えます。







現在、例外の何が問題になっていますか?



C ++例外メカニズムは1992年に開発されました。 さまざまな実装オプションが提案されています。 これらのうち、プログラム実行のメインパスにオーバーヘッドがないことを保証する例外テーブルのメカニズムが選択されました。 なぜなら、それらが作成された瞬間から、 例外は非常にまれにしかスローされないと想定されていたからです。







動的な(つまり、定期的な)例外の欠点:







  1. スローされた例外の場合、オーバーヘッドは平均で約10,000〜100,000 CPUサイクルであり、最悪の場合、約ミリ秒に達する可能性があります。
  2. バイナリファイルサイズが15〜38%増加
  3. Cプログラミングインターフェイスと互換性がない
  4. noexcept



    を除くすべての関数での暗黙的な例外スローのサポート。 関数の作成者が予期しない場合でも、プログラム内のほとんどどこでも例外をスローできます。


これらの欠点により、例外の範囲は大幅に制限されています。 例外を適用できない場合:







  1. 決定論が重要な場合、つまり、コードが通常よりも10、100、1000倍遅く動作することが許容できない場合
  2. マイクロコントローラーなど、ABIでサポートされていない場合
  3. コードの大部分がCで記述されている場合
  4. レガシーコードが大量にある会社( Google Style GuideQt )。 コードに少なくとも1つの非例外セーフ関数がある場合、平均の法則に従って、遅かれ早かれ例外がスローされ、バグが作成されます
  5. 例外の安全性についてまったく知らないプログラマーを雇っている企業


調査によると、52%(!)の開発者の職場では、企業の規則により例外が禁止されています。







しかし、例外はC ++の不可欠な部分です! -fno-exceptions



フラグを含めると、開発者は標準ライブラリの重要な部分を使用できなくなります。 これにより、企業は独自の「標準ライブラリ」を作成し、はい、独自の文字列クラスを発明するようになります。







しかし、これで終わりではありません。 例外は、コンストラクターでオブジェクトの作成をキャンセルしてエラーをスローする唯一の標準的な方法です。 それらがオフになると、2フェーズ初期化などの憎悪が現れます。 演算子もエラーコードを使用できないため、 assign



などの関数に置き換えられます。







提案:未来の例外



新しい例外転送メカニズム



P709のハーブサッターは、新しい例外転送メカニズムについて説明しました。 原則として、関数はstd::expected



返しますが、 bool



型の個別の識別子の代わりに、アライメントと共にスタックで最大8バイトを占有します。このビットの情報は、キャリーフラグなど、より高速な方法で送信されます。







CFに触れない関数(ほとんど)は、通常の戻りの場合と例外をスローする場合の両方で、静的例外を無料で使用する機会を得ます! 保存および復元を強制される関数は、最小限のオーバーヘッドを受け取りますが、 std::expected



および通常のエラーコードよりも高速です。







静的例外は次のようになります。







 int safe_divide(int i, int j) throws(arithmetic_errc) { if (j == 0) throw arithmetic_errc::divide_by_zero; if (i == INT_MIN && j == -1) throw arithmetic_errc::integer_divide_overflows; return i / j; } double foo(double i, double j, double k) throws(arithmetic_errc) { return i + safe_divide(j, k); } double bar(int i, double j, double k) { try { cout << foo(i, j, k); } catch (erithmetic_errc e) { cout << e; } }
      
      





代替バージョンでは、 throws



関数呼び出しと同じ式にtry



キーワードを設定することをおtry i + safe_divide(j, k)



ます: try i + safe_divide(j, k)



。 これにより、例外に対して安全でないコードでthrows



関数を使用するケースの数がほぼゼロになります。 いずれの場合でも、動的な例外とは異なり、IDEには例外をスローする式を何らかの方法で強調表示する機能があります。







スローされた例外は個別に格納されず、返された値の場所に直接置かれるという事実は、例外のタイプに制限を課します。 まず、簡単に再配置できる必要があります。 第二に、そのサイズはあまり大きくてはいけません(ただし、 std::unique_ptr



ようなものにすることができます)。







status_code



Niall Douglasが開発した<system_error2>



ライブラリには、 status_code<T>



-"new、better" error_code



が含まれます。 error_code



の主な違い:







  1. status_code



    静的例外を使用せずに、( status_code_category



    へのポインターとともに)考えられるほとんどすべてのエラーコードを格納するために使用できるテンプレートタイプ
  2. T



    は、簡単に再配置可能およびコピー可能にする必要があります(後者、IMHOは必須ではありません)。 コピーおよび削除するとき、仮想関数はstatus_code_category



    から呼び出されstatus_code_category



  3. status_code



    は、エラーデータだけでなく、正常に完了した操作に関する追加情報も格納できます。
  4. 「仮想」関数code.message()



    std::string



    返しませんが、 string_ref



    はかなり重い文字列型で、仮想の「所有している可能性のある」 std::string_view



    です。 そこで、 string_view



    またはstring



    、またはstd::shared_ptr<string>



    、または文字列を所有する他のクレイジーな方法をstring_view



    ことができます。 Niallは、 #include <string>



    がヘッダー<system_error2>



    容認できないほど「重く」するだろうと主張してい#include <string>





次に、 errored_status_code<T>



が入力されますstatus_code<T>



ラッパーで、次のコンストラクターがあります。







 errored_status_code(status_code<T>&& code) [[expects: code.failure() == true]] : code_(std::move(code)) {}
      
      





エラー



デフォルトの例外タイプ(タイプなしでthrows



)、および他のすべてがキャストされる例外の基本タイプ( std::exception



)はerror



です。 次のように定義されます:







 using error = errored_status_code<intptr_t>;
      
      





つまり、 error



はそのような「誤った」 status_code



であり、値( value



)が1つのポインターに置かれます。 status_code_category



メカニズムにより、理論的には正しい削除、移動、およびコピーが保証されるため、どのデータ構造でもerror



で保存できerror



。 実際には、これは次のオプションのいずれかになります。







  1. 整数(int)
  2. std::exception_handle



    、つまりスローされた動的例外へのポインター
  3. status_code_ptr



    、つまりunique_ptr



    から任意のstatus_code<T>





問題は、ケース3がstatus_code<T>



error



戻す機会を与えることを計画していないことです。 できることはmessage()



packed status_code<T>



message()



取得することだけです。 error



でラップされた値を取得できるようにするには、動的な例外としてスローする必要があります(!)。次に、 error



キャッチしてラップする必要がありerror



。 一般に、Niallは、エラーコードと文字列メッセージのみをerrorに格納する必要があると考えていerror



。これは、どのプログラムにとっても十分です。







さまざまなタイプのエラーを区別するには、「仮想」比較演算子を使用することをお勧めします。







 try { open_file(name); } catch (std::error e) { if (e == filesystem_error::already_exists) { return; } else { throw my_exception("Unknown filesystem error, unable to continue"); } }
      
      





複数のcatchブロックまたはdynamic_cast



を使用して例外のタイプを選択すると失敗します!







動的な例外との相互作用



関数には、次の仕様のいずれかがあります。









throws



noexcept



throws



意味しnoexcept



。 動的な例外が「静的」関数からスローされると、 error



ラップされerror



。 静的な例外が「動的な」関数からスローされると、 status_error



例外にラップされます。 例:







 void foo() throws(arithmetic_errc) { throw erithmetic_errc::divide_by_zero; } void bar() throws { //  arithmetic_errc   intptr_t //     error foo(); } void baz() { // error    status_error bar(); } void qux() throws { // error    status_error baz(); }
      
      





Cの例外?!



この提案では、将来のC標準の1つに例外を追加することが規定されており、これらの例外はC ++静的例外とABI互換になります。 std::expected<T, U>



似た構造std::expected<T, U>



ますが、ユーザーは独立して宣言する必要がありますが、冗長性はマクロを使用して削除できます。 構文は、(簡単にするため、これを想定します)キーワードが失敗、失敗、キャッチで構成されています。







 int invert(int x) fails(float) { if (x != 0) return 1 / x; else return failure(2.0f); } struct expected_int_float { union { int value; float error; }; _Bool failed; }; void caller() { expected_int_float result = catch(invert(5)); if (result.failed) { print_error(result.error); return; } print_success(result.value); }
      
      





同時に、C ++では、Cからfails



関数を呼び出して、それらをextern C



ブロックで宣言することもできます。 したがって、C ++では、例外を処理するための一連のキーワードがあります。









そのため、C ++では、エラー処理用の新しいツールのカートを導入(または導入)しました。 次に、論理的な質問が発生します。







何を使用するのか?



一般的な方向



エラーはいくつかのレベルに分けられます。









標準ライブラリでは、コンパイルを「例外なしで」合法にするために、動的例外の使用を完全に放棄することが最も信頼できます。







errno



errno



を使用してCおよびC ++エラーコードをすばやく簡単に処理する関数は、それぞれthrows(std::errc)



fails(int)



およびthrows(std::errc)



に置き換える必要があります。 しばらくの間、標準ライブラリの関数の古いバージョンと新しいバージョンが共存し、古いバージョンは廃止されたと宣言されます。







メモリー不足



メモリ割り当てエラーは、 new_handler



グローバルフックによって処理されます。







  1. メモリ不足を解消し、実行を継続する
  2. 例外を投げる
  3. クラッシュプログラム


現在、 std::bad_alloc



デフォルトstd::bad_alloc



スローされます。 デフォルトでstd::terminate()



を呼び出すことをお勧めします。 古い動作が必要な場合、ハンドラをmain()



先頭で必要なものに置き換えます。







標準ライブラリの既存の関数はすべてnoexcept



なり、 std::bad_alloc



ときにプログラムがクラッシュしstd::bad_alloc



。 同時に、 vector::try_push_back



などの新しいAPIが追加され、メモリ割り当てエラーが許可されます。







logic_error





例外std::logic_error



std::domain_error



std::invalid_argument



std::length_error



std::out_of_range



std::future_error



は、関数の前提条件の違反を報告します。 新しいエラーモデルでは、代わりにコントラクトを使用する必要があります。 リストされているタイプの例外は非推奨でありませんが、標準ライブラリで使用されるほとんどすべてのケースは[[expects: …]]



置き換えられます。







現在の提案状況



プロポーザルは現在ドラフト状態です。 それはすでにかなり大きく変化しており、まだ大きく変化する可能性があります。 一部の開発は公開されなかったため、提案されたAPI <system_error2>



完全には関連していません。







提案は3つの文書で説明されています。







  1. P709-サッターの国章のオリジナル文書
  2. P1095 -Niall Douglas Visionの例外の決定、いくつかの点の変更、C言語の互換性の追加
  3. P1028 - std::error



    テスト実装からのAPI std::error





現在、静的な例外をサポートするコンパイラはありません。 したがって、ベンチマークを作成することはまだできません。







C++23. , , , C++26, , , .







おわりに



, , . , . .







, ^^








All Articles