数週間前は、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 ++の例外を使用すると
不要な
なくてもエラーを集中的に処理できます。
しかし、これには多くの問題が発生します。
- 例外処理に伴うオーバーヘッドは非常に大きいため、例外をスローすることはできません。
- コンストラクタ/デストラクタから例外をスローせず、RAIIを観察することをお勧めします。
- 関数の署名によって、どの例外が関数から飛び出す可能性があるかを理解することは不可能です。
- 追加の例外サポートコードにより、バイナリファイルのサイズが増加します。
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; }
よく見えませんか?
- 関数の実際の値を返すことはできません。
- エラーの処理を忘れることは非常に簡単です(printfの戻りコードを最後にチェックしたのはいつですか?)。
- 各関数の横にエラー処理コードを記述する必要があります。 そのようなコードは読みにくいです。
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つのパラメーターがあります。
-
ReusltType
は期待値です。 -
ErrorType
エラーのタイプ。
std::expected
は、期待値またはエラーのいずれかを含むことができます。 このタイプでの作業は次のようになります。
std::expected<int, string> ok = 0; expected<int, string> notOk = std::make_unexpected("something wrong");
これは通常の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では、考えられるすべてのメソッドが表示される可能性があります。