C++ の柔軟なエラー処理技術

動作しないことがあります。ユーザーが間違った形式で入力したり、ファイルが見つからなかったり、ネットワーク接続が失敗したり、システムのメモリが不足したりすることがあります。これらはエラーであり、対処する必要があります。

高レベル関数では、これは比較的簡単です。理由が正確にわかります。 何かが間違っていて、それを正しい方法で処理できます。しかし、低レベル関数の場合、これはそれほど簡単ではありません。何がわからない 間違っていた、彼らはそれしか知らない 何か問題があり、発信者に報告する必要があります。

C++ には、エラー リターン コードと例外という 2 つの主な戦略があります。エラーを処理する「現代的」で主流の C++ の方法は例外です。 /P>

このブログ投稿では、どちらか一方を選ぶつもりはありません。代わりに、どちらの側も (比較的) 幸せにするテクニックについて説明しています。これらのテクニックは、ライブラリを開発している場合に特に役立ちます。

問題

おそらくご存知のように、私は foonathan/memory に取り組んでいます.さまざまなアロケータ クラスを提供するので、例として割り当て関数の設計を考えてみましょう.

簡単にするために 07 を考えてみましょう 割り当てられたメモリへのポインタを返します。ただし、これ以上メモリを割り当てることができなかった場合は、13 を返します。 えっ 23 、つまりエラー値。

ただし、これにはいくつかの欠点があります。すべてをチェックする必要があります 31 への呼び出し .それを忘れると、存在しないメモリを使用することになり、これは悪いことです™.また、エラー コードは本質的に推移的です。エラー コード自体を返す必要があります。

これにより、通常のコード パスとエラー コード パスが交互に配置されたコードになります。例外は、より優れた代替手段と見なすことができます。

このような場合の例外には、非常に大きな利点もあります。割り当て関数は、有効なメモリを返すか、まったく返さないかのいずれかです。これは「すべてを実行するか何もしない」関数であり、戻り値は常に有効です。Scott Meyer の「Make interfacesこれは良いことです。

これらの理由から、エラー処理メカニズムとして例外を使用する必要があると主張することができます.そして、これは私を含むほとんどのC++開発者の意見です.しかし、アロケータを提供するライブラリとして、リアルタイムアプリケーションを目指しています.多くの開発者にとってそれらのアプリケーション (特にゲーム プログラマー) が例外を使用することは例外です。

したがって、それらの開発者を喜ばせるには、私のライブラリで例外を使用しないのが最善ですが、私や他の何人かは、エラー処理のエレガントでシンプルな方法として例外を好みます。 .

では、私は何をすればよいのでしょうか?

理想的な解決策は、必要に応じて例外を有効または無効にするオプションがある場合です。例外が好きな人は例外を使用でき、そうでない人はそうする必要はありません。ただし、例外の性質上、単純に交換することはできません内部コードは例外の透過的な性質に依存しているため、エラー コードをチェックする内部コードがないためです。また、エラー コードを内部で使用し、必要に応じて例外に変換することが可能である場合でも、多くのエラーが失われます。例外の利点。

幸いなことに、メモリ不足エラーが発生したときに実際に何をするかを考えてみると、私は特別な立場にいます。ほとんどの場合、プログラムをログに記録して中止します。通常、メモリがないと正常に動作しないためです。これらの場合の例外は単純です。ロギングとアボートを行う別のコードに制御を移す方法です。しかし、このような制御を移す古くて強力な方法があります:関数ポインタ、つまりハンドラ関数です。

例外を有効にしている場合は、例外をスローするだけです。それ以外の場合は、ハンドラ関数を呼び出して、後でプログラムを中止します。最後の中止は、プログラムを通常どおり続行させることを目的とした何もしないハンドラ関数を防ぐため、重要です。これは、関数の本質的な事後条件に違反するため、致命的です:常に有効なポインターを返します.他のコードはそれに依存することができます.結局、それは正常な動作です.

私はこの手法を例外ハンドラと呼んでおり、これがメモリ内で使用したものです。

解決策 I:例外ハンドラ

最も一般的な処理動作が単に「ログ アンド アボート」であるエラーを処理する必要がある場合は、例外ハンドラーを使用できます。例外ハンドラーは、例外オブジェクトをスローする代わりに呼び出されるハンドラー関数です。ハンドラー管理を例外クラスに入れ、 48 をラップすることにより、既存のコードでも非常に簡単に実装できます。 マクロ内のステートメント。

まず、例外クラスを拡張し、ハンドラー関数を設定および照会するための関数を追加します。標準ライブラリーが 53 を処理するのと同様の方法で行うことをお勧めします。 、つまり次のように:

class my_fatal_error
{
public:
 // handler type, should take the same parameters as the constructor
 // in order to allow the same information
 using handler = void(*)( ... );

 // exchanges the handler function
 handler set_handler(handler h);

 // returns the current handler
 handler get_handler();

 ... // normal exception stuff
};

例外が有効になっている場合は、条件付きコンパイルを使用してハンドラーを削除することもできます。必要に応じて、必要な機能を提供する汎用 mixin クラスを作成することもできます。

エレガンスは例外コンストラクターです。パラメーターから必要な引数を渡して現在のハンドラー関数を呼び出します。次に、それを次の 68 と組み合わせます。 マクロ:

#if EXCEPTIONS
 #define THROW(Ex) throw (Ex)
#else
 #define THROW(Ex) (Ex), std::abort()
#endif

次のように使用できます:

THROW(my_fatal_error(...))

例外サポートが有効になっている場合、例外オブジェクトが作成され、通常どおりスローされます。ただし、例外サポートが有効になっていない場合は、例外オブジェクトも作成されます。これは重要です。その後、 70<を呼び出します。 /コード> .そして、コンストラクターがハンドラー関数を呼び出すため、必要に応じて機能します:エラーをログに記録するためのカスタマイズポイントがあります.そして 83 のため コンストラクターの後、ユーザーは事後条件を弱体化できません。

一部を許可する例外を有効にしていない場合、この手法によりフォールバックが可能になります。 もちろん、これは完全な代替品ではありません:ログアンドアボートの場合のみです.その後は続行できません.しかし、メモリ不足やその他の状況では、これは実行可能な代替品です.

しかし、例外の後も続行したい場合はどうすればよいでしょうか?

その後のコードの事後条件のため、例外ハンドラー技術ではそれが許可されません。では、この動作を有効にするにはどうすればよいでしょうか?

簡単な答えは:できない.

実行可能なオプションは 1 つだけです。2 つの機能を提供します。 1 つはエラー コードを返し、もう 1 つはスローします。例外が必要なクライアントはスロー バージョンを使用し、そうでないクライアントはエラー コード バージョンを使用します。

例として、メモリ割り当て関数をもう一度取り上げます。この場合、次の関数を使用します:

void* try_malloc(..., int &error_code) noexcept;

void* malloc(...);

最初のバージョンは 99 を返します 割り当てが失敗し、103 が設定された場合 エラー コードに。2 番目のバージョンは 110 を返しません。 ただし、代わりにスローします。最初のバージョンに関して、2 番目のバージョンを実装するのは非常に簡単であることに注意してください:

void* malloc(...)
{
 auto error_code = 0;
 auto res = try_malloc(..., error_code);
 if (!res)
 throw malloc_error(error_code);
 return res;
}

これを逆にしないでください。121 する必要があります これにより、例外サポートなしでコンパイルすることもできなくなります。示されているように実行すると、条件付きコンパイルによって他のオーバーロードを簡単に削除できます。

また、例外サポートが有効になっている場合でも、クライアントはスローしないバージョンを必要とします。たとえば、この例で可能な最大サイズを割り当てる必要がある場合です。ループで呼び出して条件付きでチェックする方が簡単で高速です。それを検出するために例外をキャッチするよりも.

解決策 II:2 つのオーバーロードを提供する

例外ハンドラーが十分でない場合は、2 つのオーバーロードを提供する必要があります。1 つのオーバーロードはリターン コードを使用し、もう 1 つは例外をスローします。

問題の関数に戻り値がある場合は、単に戻り値を使用してエラー コードを転送できます。それ以外の場合は、136 のような「無効な」値を返す必要があります。 上記の例では、呼び出し元にさらに情報を提供する場合は、エラーを通知し、出力パラメーターをエラー コードに設定します。

戻り値に失敗を示す無効な値がない場合は、144 の使用を検討してください。 - 利用可能になったら - または同様の方法で。

例外のオーバーロードは、上記のエラー コード バージョンに関して実装できます (実装する必要があります)。例外なしでコンパイルすると、条件付きコンパイルによってこのオーバーロードを消去できます。

これはもっとうまくいきますが、少なくとも例外オーバーロードを実装するときは、エラー コード バージョンを内部的に呼び出して変換するだけで済みます。

std::system_error

この種のシステムは、C++11 エラー コード機能に最適です。

153 を追加します これは、移植性のないエラー コードです。 OS 関数によって返されます。ライブラリ機能とエラー カテゴリの複雑なシステムにより、独自のエラー コードまたは 162 を追加できます。 移植可能なバージョンです。ここで紹介を読んでください。

適切な場合は、175 を使用できます エラーコード関数で。例外関数には、適切な例外クラスがあります:185 .199かかります これらのエラーを例外として報告するために使用されます。

OS 関数のラッパーであるすべての低レベル関数は、この機能または類似の機能を使用する必要があります。これは、複雑ではありますが、OS エラー コード機能を置き換える優れた機能です。

std::expected

上記のように、エラーを通知するために使用できる無効な値を持つ戻り値がない場合、問題があります。さらに、出力パラメーターはエラー コードを取得するのに適していません。

N4109 は解決策を提案します:203 .これは、戻り値またはエラー コードのいずれかを格納するクラス テンプレートです。上記の例では、次のように使用されます:

std::expected<void*, std::error_code> try_malloc(...);

成功時、218 メモリへの null 以外のポインタを格納し、失敗すると 229 を格納します .この手法は、どの戻り値でも機能するようになりました。238 のペア + 例外関数は、あらゆるユース ケースを確実に許可します。

結論

ライブラリの作成者として、クライアントに最大限の柔軟性を提供する必要がある場合があります。これには、エラー処理機能が含まれます。エラー リターン コードが必要な場合もあれば、例外が必要な場合もあります。

これらのニーズに対応するための 1 つの戦略は、例外ハンドラーです。必要に応じてスローされる例外ではなく、コールバックが呼び出されるようにするだけです。これは、とにかく終了する前にログに記録される致命的なエラーの代わりです。そのため、どこでも機能するわけではなく、同じプログラムで両方のバージョンを簡単に切り替えることはできません。これは、例外サポートを無効にするための単なる回避策です。

より柔軟な解決策は、単純に 2 つのオーバーロード (1 つは例外あり、もう 1 つは例外なし) を提供する場合です。その場合、ユーザーは最大限の自由を得ることができ、それぞれの状況に最適なバージョンを選択できます。欠点は、ライブラリの実装者としてより多くの作業を行う必要があることです。 .