C ++ 2aのエラー処理はどうなりますか

画像







数週間前は、C ++の世界での主要なカンファレンスであるCPPCONでした

午前8時から午後10時までの5日間連続して報告がありました。 すべての信仰を持つプログラマーは、C ++の将来、有毒なバイクについて議論し、C ++を簡単にする方法を考えました。







驚くべきことに、多くのレポートがエラー処理に当てられていました。 確立されたアプローチでは、最大のパフォーマンスを達成したり、コードシートを生成したりすることはできません。

C ++ 2aではどのような革新が待っていますか?







理論のビット



従来、プログラム内のすべてのエラー状況は、2つの大きなグループに分けることができます。









致命的なエラー



その後、実行を継続することは意味がありません。

たとえば、これはNULLポインターの逆参照、メモリの通過、0での除算、またはコード内の他の不変条件への違反です。 問題が発生したときに行う必要があるのは、問題に関する最大限の情報を提供し、プログラムを完了することです。







C ++で 多すぎる プログラムを完了するにはすでに十分な方法があります。









ライブラリは、クラッシュに関するデータを収集し始めているようです( 1、2、3 )。







致命的でないエラー



これらは、プログラムのロジックによって提供されるエラーです。 たとえば、ネットワークでの作業中のエラー、無効な文字列の数値への変換など。 プログラムでのこのようなエラーの出現は、物事の順序です。 その処理については、C ++で一般的に受け入れられているいくつかの戦術があります。

簡単な例を使用して、それらについて詳しく説明します。







エラー処理のさまざまなアプローチを使用して、 void addTwo()



関数を記述してみましょう。

関数は2行を読み取り、それらをint



に変換して合計を出力します。 IOエラー、オーバーフロー、および数値への変換を処理する必要があります。 面白くない実装の詳細は省略します。 3つの主なアプローチを検討します。







1.例外



 //     //   IO  std::runtime_error std::string readLine(); //    int //     std::invalid_argument int parseInt(const std::string& str); //  a  b //     std::overflow_error int safeAdd(int a, int b); void addTwo() { try { std::string aStr = readLine(); std::string bStr = readLine(); int a = parseInt(aStr); int b = parseInt(bStr); std::cout << safeAdd(a, b) << std::endl; } catch(const std::exeption& e) { std::cout << e.what() << std::endl; } }
      
      





C ++の例外を使用すると



不要な



なくてもエラーを集中的に処理できます。

しかし、これには多くの問題が発生します。









2.戻りコード



Cから継承された古典的なアプローチ







 bool readLine(std::string& str); bool parseInt(const std::string& str, int& result); bool safeAdd(int a, int b, int& result); void processError(); void addTwo() { std::string aStr; int ok = readLine(aStr); if (!ok) { processError(); return; } std::string bStr; ok = readLine(bStr); if (!ok) { processError(); return; } int a = 0; ok = parseInt(aStr, a); if (!ok) { processError(); return; } int b = 0; ok = parseInt(bStr, b); if (!ok) { processError(); return; } int result = 0; ok = safeAdd(a, b, result); if (!ok) { processError(); return; } std::cout << result << std::endl; }
      
      





よく見えませんか?







  1. 関数の実際の値を返すことはできません。
  2. エラーの処理を忘れることは非常に簡単です(printfの戻りコードを最後にチェックしたのはいつですか?)。
  3. 各関数の横にエラー処理コードを記述する必要があります。 そのようなコードは読みにくいです。

    C ++ 17およびC ++ 2aを使用すると、これらの問題がすべて順番に修正されます。


3. C ++ 17およびnodiscard



nodiscard



C ++ 17で nodiscard



ました。

関数宣言の前に指定すると、戻り値のチェックが行われないため、コンパイラの警告が発生します。







 [[nodiscard]] bool doStuff(); /* ... */ doStuff(); //  ! bool ok = doStuff(); // .
      
      





クラス、構造体、または列挙クラスにnodiscard



を指定することもできます。

この場合、属性アクションはnodiscard



というラベルのタイプの値を返すすべての関数に拡張されnodiscard









 enum class [[nodiscard]] ErrorCode { Exists, PermissionDenied }; ErrorCode createDir(); /* ... */ createDir();
      
      





nodiscard



コードを提供しません。







C ++ 17 std ::オプション



C ++ 17では、 std::optional<T>





コードがどのように見えるか見てみましょう。







 std::optional<std::string> readLine(); std::optional<int> parseInt(const std::string& str); std::optional<int> safeAdd(int a, int b); void addTwo() { std::optional<std::string> aStr = readLine(); std::optional<std::string> bStr = readLine(); if (aStr == std::nullopt || bStr == std::nullopt){ std::cerr << "Some input error" << std::endl; return; } std::optional<int> a = parseInt(*aStr); std::optional<int> b = parseInt(*bStr); if (!a || !b) { std::cerr << "Some parse error" << std::endl; return; } std::optional<int> result = safeAdd(*a, *b); if (!result) { std::cerr << "Integer overflow" << std::endl; return; } std::cout << *result << std::endl; }
      
      





関数からin-out引数を削除すると、コードがよりきれいになります。

ただし、エラー情報は失われています。 いつ、何が悪かったのかが不明確になりました。

std::optional



std::variant<ResultType, ValueType>



置き換えることができます。

コードの意味は、 std::optional



と同じstd::optional



が、より面倒です。







C ++ 2aおよびstd ::予想



std::expected<ResultType, ErrorType>



- 特別なテンプレートタイプで、最も近い不完全な標準に分類される場合があります。

2つのパラメーターがあります。









これは通常のvariant



とどう違うのですか? 何が特別なのですか?

std::expected



されるのはモナドです。

モナドのようにstd::expected



れるstd::expected



catch_error



の操作をサポートすることが提案std::expected



れています: map



catch_error



bind



unwrap



return



そしてthen





これらの関数を使用して、関数呼び出しをチェーンにチェーンできます。







 getInt().map([](int i)return i * 2;) .map(integer_divide_by_2) .catch_error([](auto e) return 0; );
      
      





std::expected



を返す関数があるとします。







 std::expected<std::string, std::runtime_error> readLine(); std::expected<int, std::runtime_error> parseInt(const std::string& str); std::expected<int, std::runtime_error> safeAdd(int a, int b);
      
      





以下は擬似コードのみであり、最新のコンパイラで強制的に動作させることはできません。

Haskellからモナドの操作記録するための構文を借用することができます 。 許可しないのはなぜですか:







 std::expected<int, std::runtime_error> result = do { auto aStr <- readLine(); auto bStr <- readLine(); auto a <- parseInt(aStr); auto b <- parseInt(bStr); return safeAdd(a, b) }
      
      





一部の著者はこの構文を提案しています:







 try { auto aStr = try readLine(); auto bStr = try readLine(); auto a = try parseInt(aStr); auto b = try parseInt(bStr); std::cout result << std::endl; return safeAdd(a, b) } catch (const std::runtime_error& err) { std::cerr << err.what() << std::endl; return 0; }
      
      





コンパイラは、このようなコードブロックを関数呼び出しのシーケンスに自動的に変換します。 ある時点で、関数が期待したものを返さない場合、計算チェーンが中断します。 また、エラータイプとして、標準に既に存在する例外タイプを使用できますstd::out_of_range



std::runtime_error



std::out_of_range



など。







構文をうまく設計できれば、 std::expected



は単純で効率的なコードを書くことができます。







おわりに



エラーを処理する理想的な方法はありません。 最近まで、C ++には、モナドを除くほとんどすべてのエラー処理方法がありました。

C ++ 2aでは、考えられるすべてのメソッドが表示される可能性があります。







トピックで何を読んで見るか



  1. 実際の提案
  2. CPPCONで予想されるstd ::に関するスピーチ
  3. Andrei Alexandrescu std :: about C ++ Russiaで期待されています。
  4. Redditの提案に関する最近の議論



All Articles