私は長い間CとC++をプログラムしてきましたが、これまで例外を使用してtry/catchを実行したことはありません。関数にエラーコードを返す代わりにそれを使用する利点は何ですか?
13 に答える
おそらく明らかなポイント - 開発者はあなたの返品ステータスを無視 (または認識せず) し、何かが失敗したことに気付かずに幸せに過ごすことができます。
何らかの方法で例外を確認する必要があります。積極的に何かを配置しない限り、黙って無視することはできません。
例外の利点は 2 つあります。
それらを無視することはできません。 何らかのレベルでそれらに対処する必要があります。そうしないと、プログラムが終了します。エラー コードがある場合は、それらを明示的に確認する必要があります。そうしないと、それらが失われます。
それらは無視できます。 エラーが 1 つのレベルで処理できない場合、次のレベルに自動的にバブルアップされます。エラー コードは、処理できるレベルに達するまで明示的に渡す必要があります。
利点は、失敗する可能性のある呼び出しのたびにエラー コードを確認する必要がないことです。ただし、これを機能させるには、スタックが巻き戻されるときにすべてが自動的にクリーンアップされるように、RAII クラスと組み合わせる必要があります。
エラーメッセージあり:
int DoSomeThings()
{
int error = 0;
HandleA hA;
error = CreateAObject(&ha);
if (error)
goto cleanUpFailedA;
HandleB hB;
error = CreateBObjectWithA(hA, &hB);
if (error)
goto cleanUpFailedB;
HandleC hC;
error = CreateCObjectWithA(hB, &hC);
if (error)
goto cleanUpFailedC;
...
cleanUpFailedC:
DeleteCObject(hC);
cleanUpFailedB:
DeleteBObject(hB);
cleanUpFailedA:
DeleteAObject(hA);
return error;
}
例外と RAII あり
void DoSomeThings()
{
RAIIHandleA hA = CreateAObject();
RAIIHandleB hB = CreateBObjectWithA(hA);
RAIIHandleC hC = CreateCObjectWithB(hB);
...
}
struct RAIIHandleA
{
HandleA Handle;
RAIIHandleA(HandleA handle) : Handle(handle) {}
~RAIIHandleA() { DeleteAObject(Handle); }
}
...
一見すると、クリーンアップ コードを 1 回だけ記述する必要があることに気付くまでは、RAII/Exceptions バージョンの方が長いように見えます (それを単純化する方法があります)。しかし、DoSomeThings の 2 番目のバージョンは、はるかに明確で保守しやすいものです。
リソースとメモリをリークするため、RAII イディオムを使用せずに C++ で例外を使用しようとしないでください。すべてのクリーンアップは、スタック割り当てオブジェクトのデストラクタで行う必要があります。
エラー コードを処理する方法は他にもあると思いますが、最終的にはどれも同じように見えます。goto を削除すると、クリーンアップ コードを繰り返すことになります。
エラー コードのポイントの 1 つは、どこでエラーが発生し、どのようにエラーが発生するかを明確にすることです。上記のコードでは、失敗しないことを前提として記述しています (失敗した場合でも、RAII ラッパーによって保護されます)。しかし、どこで問題が発生する可能性があるかについては、あまり注意を払わなくなります。
例外処理は、プログラムの機能を処理するために記述されたコードからエラー処理コードを簡単に分離できるため便利です。これにより、コードの読み取りと書き込みが簡単になります。
言及された他のことを除いて、コンストラクターからエラーコードを返すことはできません。デストラクタも同様ですが、デストラクタからも例外をスローしないようにする必要があります。
- 場合によってはエラー状態が予想される場合にエラーコードを返します
- いずれの場合もエラー状態が予想されない場合は例外をスローします
前者の場合、関数の呼び出し元は、予想される失敗についてエラーコードをチェックする必要があります。後者の場合、例外は、スタックの上の任意の呼び出し元(またはデフォルトのハンドラー)が適切に処理できます。
これについてのブログ エントリ ( Exceptions make for Elegant Code ) を書き、その後Overloadに掲載されました。これは、Joel が StackOverflow ポッドキャストで言ったことに応えて書いたものです。
とにかく、ほとんどの状況では、エラー コードよりも例外の方が望ましいと強く信じています。エラー コードを返す関数を使用するのは非常に面倒です。呼び出しのたびにエラー コードを確認する必要があるため、呼び出しコードの流れが中断される可能性があります。また、エラーを通知する方法がないため、オーバーロードされた演算子を使用できないことも意味します。
エラー コードをチェックする手間は、人々がそれを無視することが多いことを意味し、完全に無意味になります。少なくとも、catch
ステートメントで例外を明示的に無視する必要があります。
C++ ではデストラクタを使用し、.NET ではディスポーザを使用して、例外が発生した場合にリソースが正しく解放されるようにすることで、コードを大幅に簡素化することもできます。エラーコードで同じレベルの保護を得るにはif
、多くのステートメント、複製されたクリーンアップコード、またはgoto
関数の最後でのクリーンアップの共通ブロックへの呼び出しが必要です。これらのオプションはどれも快適ではありません。
私がC++を教えていたとき、私たちの標準的な説明は、晴れた日と雨の日のシナリオが絡まないようにすることができるというものでした。つまり、すべてが正常に機能するかのように関数を記述し、最後に例外をキャッチすることができます。
例外なく、各呼び出しから戻り値を取得し、それがまだ正当であることを確認する必要があります。
もちろん、関連する利点は、例外で戻り値を「無駄にする」ことがなく(したがって、voidであるはずのメソッドをvoidにすることができる)、コンストラクタとデストラクタからエラーを返すこともできることです。
EAFP (「許可よりも許しを求める方が簡単」) についての良い説明があります。Wikipedia の Python ページであっても、ここに当てはまると思います。例外を使用すると、より自然なコーディング スタイル (IMO) につながります。また、他の多くの人の意見でもあります。
Google の C++ スタイル ガイドには、C++ コードで例外を使用することの長所と短所に関する優れた徹底的な分析があります。また、尋ねるべきより大きな質問のいくつかを示しています。つまり、自分のコードを他の人 (例外が有効なコード ベースとの統合が困難な人) に配布するつもりですか?
例外的なケースにフラグを立てるために、実際に例外を使用する必要がある場合があります。たとえば、コンストラクターで何か問題が発生し、これについて呼び出し元に通知することが理にかなっている場合は、例外をスローするしかありません。
別の例: 関数がエラーを示すために返すことができる値がない場合があります。関数が返す可能性のある値はすべて、成功を示します。
int divide(int a, int b)
{
if( b == 0 )
// then what? no integer can be used for an error flag!
else
return a / b;
}
例外を認めなければならないという事実は正しいですが、これはエラー構造体を使用して実装することもできます。特定のメソッド ( IsOk など) が呼び出されたかどうかを dtor でチェックする基本エラー クラスを作成できます。そうでない場合は、何かをログに記録してから終了するか、例外をスローするか、アサートを発生させるなど...
反応せずにエラー オブジェクトに対して IsOk を呼び出すだけでは、catch( ... ) {} を記述するのと同じになります。どちらのステートメントも、同じようにプログラマーの善意の欠如を示しています。
エラー コードを正しいレベルまで転送することは、より重要な問題です。基本的に、伝播の唯一の理由で、ほとんどすべてのメソッドがエラー コードを返すようにする必要があります。しかし、繰り返しますが、関数またはメソッドには、それが生成する可能性のある例外で常に注釈を付ける必要があります。したがって、基本的には、それをサポートするインターフェースがなくても、同じ問題が発生する必要があります。
@Martinが指摘したように、例外をスローすると、プログラマーはエラーを処理する必要があります。たとえば、リターンコードをチェックしないことは、Cプログラムのセキュリティホールの最大の原因の1つです。例外は、(うまくいけば)エラーを処理し、プログラムに何らかの回復パスを提供することを確認します。また、セキュリティホールを導入するのではなく、例外を無視することを選択すると、プログラムがクラッシュします。