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つの注釈:
- 関数のパラメーターをチェックします
- 関数の戻り値を確認します(引数として受け取ります)
- assert -assertマクロの文明的な置き換え
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::terminate
コンパイラは関数コードを最適化するために契約からの保証を使用するため、契約違反後にプログラム操作を継続することは不可能です。 契約が履行されるというわずかな疑いがある場合は、追加のチェックを追加する価値があります。
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と同等である場合、クラスは自明に再配置可能と見なされます。 古いオブジェクトは削除されず、作成者は「床に落とす」と呼びます。
次の(再帰)条件のいずれかが真である場合、型はコンパイラの観点から自明に再配置可能です。
- 簡単に移動可能+簡単に破壊可能(
int
またはPOD構造など) - これは、
[[trivially_relocatable]]
属性でマークされたクラスです[[trivially_relocatable]]
- これは、すべてのメンバーが自明に再配置可能なクラスです。
この情報はstd::uninitialized_relocate
で使用できます。これは、通常の方法でmove init + deleteを実行するか、可能であれば加速します。 std::string
、 std::vector
、 std::unique_ptr
など、ほとんどのタイプの標準ライブラリを[[trivially_relocatable]]
としてマークすることをお勧めします。 オーバーヘッドstd::vector<std::unique_ptr<T>>
これを念頭に置いて提案は消えます。
現在、例外の何が問題になっていますか?
C ++例外メカニズムは1992年に開発されました。 さまざまな実装オプションが提案されています。 これらのうち、プログラム実行のメインパスにオーバーヘッドがないことを保証する例外テーブルのメカニズムが選択されました。 なぜなら、それらが作成された瞬間から、 例外は非常にまれにしかスローされないと想定されていたからです。
動的な(つまり、定期的な)例外の欠点:
- スローされた例外の場合、オーバーヘッドは平均で約10,000〜100,000 CPUサイクルであり、最悪の場合、約ミリ秒に達する可能性があります。
- バイナリファイルサイズが15〜38%増加
- Cプログラミングインターフェイスと互換性がない
-
noexcept
を除くすべての関数での暗黙的な例外スローのサポート。 関数の作成者が予期しない場合でも、プログラム内のほとんどどこでも例外をスローできます。
これらの欠点により、例外の範囲は大幅に制限されています。 例外を適用できない場合:
- 決定論が重要な場合、つまり、コードが通常よりも10、100、1000倍遅く動作することが許容できない場合
- マイクロコントローラーなど、ABIでサポートされていない場合
- コードの大部分がCで記述されている場合
- レガシーコードが大量にある会社( Google Style Guide 、 Qt )。 コードに少なくとも1つの非例外セーフ関数がある場合、平均の法則に従って、遅かれ早かれ例外がスローされ、バグが作成されます
- 例外の安全性についてまったく知らないプログラマーを雇っている企業
調査によると、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
の主な違い:
-
status_code
静的例外を使用せずに、(status_code_category
へのポインターとともに)考えられるほとんどすべてのエラーコードを格納するために使用できるテンプレートタイプ -
T
は、簡単に再配置可能およびコピー可能にする必要があります(後者、IMHOは必須ではありません)。 コピーおよび削除するとき、仮想関数はstatus_code_category
から呼び出されstatus_code_category
-
status_code
は、エラーデータだけでなく、正常に完了した操作に関する追加情報も格納できます。 - 「仮想」関数
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
。 実際には、これは次のオプションのいずれかになります。
- 整数(int)
-
std::exception_handle
、つまりスローされた動的例外へのポインター -
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
を使用して例外のタイプを選択すると失敗します!
動的な例外との相互作用
関数には、次の仕様のいずれかがあります。
-
noexcept
:例外をスローしません -
throws(E)
:静的例外のみをスローします - (なし):動的例外のみをスローします
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 ++では、例外を処理するための一連のキーワードがあります。
-
throw()
-C ++ 20で削除されました -
noexcept
関数指定子、関数は動的例外をスローしません -
noexcept(expression)
-関数指定子、関数は提供された動的な例外をスローしません -
noexcept(expression)
-式は動的な例外をスローしますか? -
throws(E)
-関数指定子、関数は静的例外をスローします -
throws
=throws(std::error)
-
fails(E)
-Cからインポートされた関数は静的例外をスローします
そのため、C ++では、エラー処理用の新しいツールのカートを導入(または導入)しました。 次に、論理的な質問が発生します。
何を使用するのか?
一般的な方向
エラーはいくつかのレベルに分けられます。
- プログラマーのエラー。 契約を使用して処理されます。 それらは、 フェイルファストの概念に従ってログの収集とプログラムの終了をもたらします。 例:nullポインター(これが無効な場合); ゼロ除算; プログラマーが予測しないメモリ割り当てエラー。
- プログラマが提供する致命的なエラー。 関数からの通常の戻り値よりも百万倍少ない頻度でスローされます。これにより、動的例外が正当化されます。 通常、このような場合、プログラムのサブシステム全体を再起動するか、操作の実行時にエラーを発生させる必要があります。 例:データベースとの接続が突然失われた。 プログラマが提供するメモリ割り当てエラー。
- 何かが関数のタスクの完了を妨げたが、呼び出し元の関数がそれをどうするかを知っている場合の回復可能なエラー。 静的例外によって処理されます。 例:ファイルシステムを操作します。 その他の入出力(IO)エラー。 不正なユーザーデータ
vector::at()
。 - 関数は、予期しない結果ではあるものの、タスクを正常に完了しました。
std::optional
、std::expected
、std::variant
ます。 例:stoi()
;vector::find()
;map::insert
。
標準ライブラリでは、コンパイルを「例外なしで」合法にするために、動的例外の使用を完全に放棄することが最も信頼できます。
errno
errno
を使用してCおよびC ++エラーコードをすばやく簡単に処理する関数は、それぞれthrows(std::errc)
fails(int)
およびthrows(std::errc)
に置き換える必要があります。 しばらくの間、標準ライブラリの関数の古いバージョンと新しいバージョンが共存し、古いバージョンは廃止されたと宣言されます。
メモリー不足
メモリ割り当てエラーは、 new_handler
グローバルフックによって処理されます。
- メモリ不足を解消し、実行を継続する
- 例外を投げる
- クラッシュプログラム
現在、 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つの文書で説明されています。
- P709-サッターの国章のオリジナル文書
- P1095 -Niall Douglas Visionの例外の決定、いくつかの点の変更、C言語の互換性の追加
- P1028 -
std::error
のテスト実装からのAPIstd::error
現在、静的な例外をサポートするコンパイラはありません。 したがって、ベンチマークを作成することはまだできません。
C++23. , , , C++26, , , .
おわりに
, , . , . .
, ^^