2つの基本的な戦略があります。修正可能なエラー(例外、エラー戻りコード、ハンドラー関数)の処理と回復不能(
assert()
、
abort()
)です。 最適な戦略はいつですか?
エラーの種類
エラーはさまざまな理由で発生します。ユーザーが奇妙なデータを入力した、OSがファイルハンドラーを提供できない、コードが
nullptr
逆参照するなどです。 説明されている各エラーには、個別のアプローチが必要です。 理由により、エラーは3つの主要なカテゴリに分類されます。
- ユーザーエラー:ここで、ユーザーとは、コンピューターの前に座って実際にプログラムを「使用」している人を意味し、APIを引き出すプログラマーではありません。 このようなエラーは、ユーザーが何か間違ったことをすると発生します。
- OSが要求を満たすことができない場合、 システムエラーが表示されます。 つまり、システムエラーの原因は、システムAPI呼び出しの失敗です。 プログラマーがシステムコールに不正なパラメーターを渡したために発生するものもあります。これは、システムエラーではなく、プログラマーのエラーです。
- プログラマーのエラーは、プログラマーがAPIまたはプログラミング言語の前提条件を考慮していない場合に発生します。 APIが、
0
を最初のパラメーターとしてfoo()
を呼び出さないことを要求している場合、プログラマーが非難します。 ユーザーがfoo()
に渡された0
入力し、プログラマーが入力データのチェックを書き込まなかった場合、これは再び彼のせいです。
説明されているエラーの各カテゴリには、その処理に対する特別なアプローチが必要です。
カスタムエラー
私は非常に大声で言います:そのような間違いは本当に間違いではありません。
すべてのユーザーが指示に従わない。 人々が入力するデータを扱うプログラマーは、それが入力されるのは悪いデータであると期待しなければなりません。 したがって、まず、有効性をチェックし、検出されたエラーについてユーザーに通知し、再入力するように依頼する必要があります。
したがって、ユーザーのエラーに処理戦略を適用しても意味がありません。 エラーが発生しないように、入力データはできるだけ早くチェックする必要があります。
もちろん、これは常に可能とは限りません。 入力を検証するには高すぎる場合もあれば、コードアーキテクチャや責任の分離を行えない場合もあります。 ただし、このような場合、エラーは修正可能なものとして一意に扱う必要があります。 そうでない場合、空のドキュメントでバックスペースをクリックしたためにオフィスプログラムがクラッシュしたとしましょう。さもないと、発射された武器を発射しようとするとゲームがクラッシュします。
修正可能なエラーを処理するための戦略として例外を好む場合は、注意してください。例外は例外的な状況のみを対象としており、ユーザーが誤ったデータを入力するほとんどの場合は含まれません。 実際、多くのアプリケーションによると、これは標準ですらあります。 例外が使用されるのは、コールスタックの深部(おそらく外部コード)で、まれに発生するか非常に重大なユーザーエラーが見つかった場合のみです。 そうでない場合は、戻りコードを使用してエラーを報告することをお勧めします。
システムエラー
通常、システムエラーは予測できません。 さらに、これらは非決定的であり、以前は問題なく動作していたプログラムで発生する可能性があります。 入力データのみに依存するユーザーエラーとは異なり、システムエラーは実際のエラーです。
しかし、それらをどのように処理しますか、どのように修正可能または修正不能ですか?
それは状況に依存します。
多くの人々は、メモリ不足エラーは致命的だと考えています。 多くの場合、このエラーを処理するためにも十分なメモリがありません! そして、すぐに実行を中断する必要があります。
しかし、OSがソケットを割り当てることができないという事実によるプログラムのクラッシュは、あまりフレンドリーな動作ではありません。 そのため、例外をスローし、
catch
てプログラムを優しく閉じてください。
ただし、例外をスローすることは常に正しい選択とは限りません。
誰かが彼が常に間違っているとさえ言うでしょう。
失敗した後に操作を繰り返したい場合、ループ内の
try-catch
で関数をラップするのは遅い解決策です。 正しい選択は、エラーコードを返し、正しい値が返されるまでループすることです。
自分専用のAPI呼び出しを作成する場合は、状況に合ったパスを選択してそれに従ってください。 ただし、ライブラリを作成している場合、ユーザーが何を望んでいるかわかりません。 次に、このケースに適した戦略を分析します。 致命的となる可能性のあるエラーには「エラーハンドラ」が適していますが、他のエラーには、イベントを開発するための2つのオプションを提供する必要があります。
デバッグモードでのみ有効になっているアサーションを使用しないでください。 結局、リリースビルドでシステムエラーが発生する可能性があります!
プログラミングエラー
これは最悪の種類のエラーです。 それらを処理するために、エラーが関数呼び出し、つまり不適切なパラメーターにのみ関連付けられるようにします。 他のタイプのプログラミングエラーは、コード全体に散らばっているデバッグマクロ(アサーションマクロ)を使用して、ランタイムでのみキャッチできます。
不適切なパラメータを使用する場合、2つの戦略があります。具体的な動作または未定義の動作を指定します。
関数の初期要件が不正なパラメーターを渡すことの禁止である場合、それらを渡すと、これは未定義の動作と見なされ、関数自体ではなく、呼び出し演算子(呼び出し元)でチェックする必要があります。 関数はデバッグアサーションのみを行う必要があります。
一方、不正なパラメーターがないことが初期要件の一部ではなく、不正なパラメーターを渡すときに関数が
bad_parameter_exception
をスローすることをドキュメントが決定している場合、渡すことは明確に定義された動作です(例外または修正可能なエラーを処理するための他の戦略をスローします)常に確認する必要があります。
例として、アクセサー関数
std::vector<T>
考えます。
operator[]
仕様は、インデックスが有効な範囲内にある必要があると言い、
at()
は、インデックスが範囲外。 さらに、ほとんどの標準ライブラリ実装は、
operator[]
インデックスがチェックされるデバッグモードを提供しますが、技術的にはこれは未定義の動作であり、チェックする必要はありません。
注:特定の動作を取得するために例外をスローする必要はありません。 関数の初期条件で言及されるまで、定義済みと見なされます。 初期条件で記述されたものはすべて、関数によってチェックされるべきではありません。これは未定義の動作です。
デバッグ確認の助けを借りてのみチェックする必要があるとき、そしていつ-常にですか?
残念ながら、明確なレシピはありません;決定は特定の状況に依存します。 APIを開発するときに従う実証済みのルールは1つしかありません。 これは、呼び出し側ではなく、呼び出し側がベースラインをチェックするという観察に基づいています。 これは、条件が呼び出し元に対して「検証可能」でなければならないことを意味します。 また、パラメータ値が常に正しい操作を簡単に実行できる場合、条件は「チェック」されます。 パラメータでこれが可能な場合、これは初期条件です。つまり、デバッグの確認によってのみチェックされます(また、高すぎる場合はまったくチェックされません)。
しかし、最終決定は他の多くの要因に依存するため、一般的なアドバイスをすることは非常に困難です。 デフォルトでは、未定義の動作に減らし、確認のみを使用しようとします。 標準ライブラリが
operator[]
および
at()
行うように、両方のオプションを提供することが推奨される場合があります。
場合によっては、これは間違いかもしれません。
std::exception
階層について
修正可能なエラーを処理するための戦略として例外を選択した場合は、新しいクラスを作成し、標準ライブラリの例外クラスの1つから継承することをお勧めします。
これらの4つのクラスのうち1つだけを継承することをお勧めします。
-
std::bad_alloc
:メモリ割り当ての失敗。 -
std::runtime_error
:一般的なランタイムエラー用。 -
std::system_error
(std::runtime_error
から派生):エラーコードを含むシステムエラー用。 -
std::logic_error
:特定の動作を伴うプログラミングエラー用。
標準ライブラリは、論理エラー(つまり、プログラマー)とランタイムエラーを分離することに注意してください。 ランタイムエラーは、システムエラーよりも広い定義です。 「プログラムの実行中にのみ検出されるエラー」について説明します。 この言葉遣いはあまり有益ではありません。 個人的には、プログラムのエラーだけでなく、ユーザーの過失によっても発生する可能性のある不良パラメーターに使用します。 ただし、これは呼び出しスタックの奥深くでのみ定義できます。 たとえば、 標準形式のコメントの形式が不適切な場合、
std::runtime_error
起因する
std::runtime_error
例外が発生し
std::runtime_error
。 その後、適切なレベルでキャッチされ、ログに記録されます。 しかし、
std::logic_error
以外ではこのクラスを使用しません。
まとめると
エラーを処理するには2つの方法があります。
- 修正可能 :例外または戻り値が使用されます(状況/宗教に応じて)。
- as fatal :エラーがログに記録され、プログラムが中断されます。
謝辞は、デバッグモードでのみ、致命的なエラー処理戦略の特別な種類です。
エラーには3つの主な原因があり、それぞれに特別なアプローチが必要です。
- ユーザーエラーは、プログラムの上位レベルでエラーとして扱われるべきではありません。 ユーザーが入力するものはすべて、それに応じてチェックする必要があります。 これは、ユーザーと直接対話しない下位レベルでのみエラーとして扱うことができます。 修正可能なエラー処理戦略が適用されます。
- システムエラーは、タイプと重大度に応じて、2つの戦略のいずれかで処理できます。 ライブラリは可能な限り柔軟でなければなりません。
- プログラマーのエラー 、つまり不適切なパラメーターは、初期条件によって禁止される場合があります。 この場合、関数はデバッグ確認を使用した検証のみを使用する必要があります。 完全に定義された動作について話している場合、関数は規定の方法でエラーを報告する必要があります。 デフォルトでは、未定義の動作のシナリオに従い、呼び出し側で行うのが困難すぎる場合にのみ、関数のパラメーターチェックを定義しようとします。
C ++での柔軟なエラー処理技術
時には何かが機能しないことがあります。 ユーザーが無効な形式でデータを入力すると、ファイルが検出されず、ネットワーク接続が失敗し、システムのメモリが不足します。 これらはすべてエラーであり、処理する必要があります。
これは、高レベル関数で比較的簡単に実行できます。 何かがうまくいかなかった理由を正確に知っており、それに応じて対処できます。 しかし、低レベル関数の場合、すべてがそれほど単純ではありません。 彼らは何が悪いのかを知らず、失敗の事実のみを知っており、それを彼らに電話した人に報告しなければなりません。
C ++には、エラー戻りコードと例外という2つの主なアプローチがあります。 今日、例外の使用が広まっています。 しかし、さまざまな理由で、使用できない/使用できないと思う/使用したくないと考える人もいます。
私は味方しません。 代わりに、両方のアプローチの支持者を満足させるテクニックを説明します。 特に、この手法はライブラリ開発者に役立ちます。
問題
私はfoonathan / memoryプロジェクトに取り組んでいます。 このソリューションはさまざまなアロケータークラスを提供するので、例として割り当て関数の構造を見てみましょう。
簡単にするために、
malloc()
ます。 割り当てられたメモリへのポインタを返します。 メモリ割り当てが失敗すると、
nullptr
、つまり
NULL
、つまり誤った値が
NULL
ます。
このソリューションには欠点があります:
malloc()
すべての呼び出しをチェックする必要があります。 これを忘れた場合は、存在しないメモリを割り当ててください。 さらに、本質的にエラーコードは推移的です。エラーコードを返すことができる関数を呼び出し、それを無視したり処理したりできない場合は、エラーコードも返す必要があります。
これにより、通常のコード分岐と誤ったコード分岐が交互に発生する状況になります。 その場合、例外がより適切なソリューションになります。 それらのおかげで、必要なときにだけエラーを処理できます。そうでない場合は、呼び出し元にエラーを返すのは非常に静かです。
これは欠点とみなすことができます。
しかし、そのような状況では、例外には非常に大きな利点もあります。メモリ割り当て関数は有効なメモリを返すか、まったく何も返しません。 これはオールオアナッシング関数であり、戻り値は常に有効です。 これは、Scott Mayerの原則によると、「 インターフェイスを誤って使いにくくし、正しく使いやすいようにする 」という有用な結果です。
上記を考えると、エラー処理メカニズムとして例外を使用する必要があると主張できます。 この意見は、私を含むほとんどのC ++開発者によって共有されています。 しかし、私が行っているプロジェクトは、メモリ割り当てツールを提供するライブラリであり、リアルタイムアプリケーション向けに設計されています。 そのようなアプリケーションのほとんどの開発者(特にigrodelov)にとって、例外の使用は例外です。
パン探偵。
この開発者グループを尊重するには、私のライブラリーが例外なく行う方が良いです。 しかし、私と他の多くの人はそれらの優雅さとエラー処理の容易さのためにそれらを好むので、他の開発者のために、私のライブラリは例外を使うほうが良いです。
それではどうしますか?
理想的なソリューション:例外を自由に有効および無効にする機能。 ただし、例外の性質を考えると、エラーコードと交換することはできません。内部エラーチェックコードがないためです。すべての内部コードは、例外が透過的であるという前提に基づいています。 また、エラーコードが内部で使用され、例外に変換されたとしても、例外の利点のほとんどが奪われてしまいます。
幸いなことに、メモリ不足エラーが発生したときに何をするかを判断できます。ほとんどの場合、このイベントを記録してプログラムを中断します。 そのような状況では、例外は単に、プログラムを記録して中断する別のコードに制御を渡す方法です。 しかし、制御を移すための古くて効率的な方法があります:関数ポインター、つまりハンドラー関数です。
例外を有効にしている場合は、それらをスローします。 それ以外の場合は、ハンドラー関数を呼び出してからプログラムを終了します。 これにより、役に立たない関数が機能しなくなり、プログラムを正常に実行し続けることができます。 中断しない場合、関数の必須の事後条件に違反します。常に有効なポインターを返します。 実際、この条件が満たされると、別のコードの作業を構築できます。実際、これは正常な動作です。
私はこのアプローチを例外処理と呼び、メモリを操作するときにそれを守ります。
解決策1:例外ハンドラー
最も一般的な動作が「ログ記録と中止」である場合の条件でエラーを処理する必要がある場合は、例外ハンドラーを使用できます。 これは、例外オブジェクトをスローする代わりに呼び出されるハンドラー関数です。 既存のコードであっても、実装は非常に簡単です。 これを行うには、例外クラスに処理コントロールを配置し、マクロで
throw
式をラップします。
最初に、クラスを追加し、ハンドラ関数を構成し、場合によってはリクエストする関数を追加します。 標準ライブラリが
std::new_handler
処理するのと同じ方法でこれを行うことをお勧めし
std::new_handler
。
class my_fatal_error { public: // , , , // using handler = void(*)( ... ); // - handler set_handler(handler h); // handler get_handler(); ... // };
これは例外クラスのスコープ内にあるため、特別な方法で名前を付ける必要はありません。 まあ、それは私たちにとって簡単です。
例外が有効な場合、条件付きコンパイルを使用してハンドラーを削除できます。 必要に応じて、必要な機能を提供する通常の混合クラス(mixinクラス)も作成します。
例外コンストラクターはエレガントです。現在のハンドラー関数を呼び出し、パラメーターから必要な引数を渡します。 そして、その後の
throw
マクロと結合します。
If```cpp #if EXCEPTIONS #define THROW(Ex) throw (Ex) #else #define THROW(Ex) (Ex), std::abort() #endif
> throw [foonathan/compatiblity](https://github.com/foonathan/compatibility). : ```cpp THROW(my_fatal_error(...))
例外サポートを有効にしている場合、例外オブジェクトが作成されてスローされます。すべてが通常どおりです。 しかし、サポートがオフになっている場合、とにかく例外オブジェクトが作成されます-これは重要です-その後のみ
std::abort()
呼び出されます。 また、コンストラクターはハンドラー関数を呼び出すため、必要に応じて機能します。エラーログのチューニングポイントを取得します。 コンストラクターの後の
std::abort()
の呼び出しのおかげで、ユーザーは事後条件を破ることができません。
メモリを操作するとき、例外が有効になっているとき、例外がスローされたときに呼び出されるハンドラもあります。
したがって、この手法を使用すると、例外をオフにしても、ある程度のカスタマイズを引き続き使用できます。 , , , . , , .
?
. ?
— . , , . , .
: . , . , , , — .
, , .
. :
void* try_malloc(..., int &error_code) noexcept; void* malloc(...);
nullptr
error_code
.
nullptr
, . , :
void* malloc(...) { auto error_code = 0; auto res = try_malloc(..., error_code); if (!res) throw malloc_error(error_code); return res; }
, , . . , , (overload) .
, . , , . , .
2:
, . , .
, . —
nullptr
, — , .
,
errno
-
GetLastError()
!
,
std::optional
- .
(exception overload) — — , . , .
std::system_error
++ 11.
(non-portable)
std::error_code
, . ,
std::error_condition
. . ,
std::error_code
. :
std::system_error
.
std::error_code
.
, -. — — , .
, . .
std::expected
, , , . , — .
!
№ 4109 :
std::expected
. , . :
std::expected<void*, std::error_code> try_malloc(...);
std::expected
-null , —
std::error_code
. .
std::expected
.
おわりに
, . : , — .
— . , , callback, . , . , . .
— , , . , , . : .