アサートまたはエラーとしての例外?


10

私はプロのCプログラマーであり、趣味のObj-Cプログラマー(OS X)です。最近、その非常に豊富な構文のために、C ++に拡張するように誘惑されました。

これまでのコーディングでは、例外をあまり扱っていません。Objective-Cにはそれらがありますが、Appleのポリシーは非常に厳格です。

重要プログラミングまたは予期しないランタイムエラー(範囲外のコレクションアクセス、不変オブジェクトの変更の試行、無効なメッセージの送信、ウィンドウサーバーへの接続の喪失など)の例外の使用を予約する必要があります。

C ++は例外をより頻繁に使用することを好むようです。たとえば、RAIIのウィキペディアの例では、ファイルを開くことができない場合に例外がスローされます。Objective-Cはreturn nil、outパラメータによってエラーが送信されます。特に、std :: ofstreamはどちらの方法でも設定できるようです。

ここで、プログラマーに対して、エラーコードの代わりに例外を使用することを宣言するか、例外をまったく使用ないことを宣言するいくつかの答えを見つけました。前者の方が一般的です。

C ++の客観的な研究をしている人は見つかりませんでした。ポインタはまれであるため、例外を回避することを選択した場合は、内部エラーフラグを使用する必要があります。それは扱いが面倒すぎますか、それとも例外よりもうまく機能しますか?両方のケースを比較するのが最良の答えです。

編集:完全に関連しているわけではありませんが、私はおそらく何nilが何であるかを明確にする必要があります。技術的にはと同じNULLですが、メッセージをに送信しても問題ありませんnil。だからあなたは次のようなことができます

NSError *err = nil;
id obj = [NSFileHandle fileHandleForReadingFromURL:myurl error:&err];

[obj retain];

最初の呼び出しが返されたとしてもnil。また*obj、Obj-Cでは絶対にしないので、NULLポインターの逆参照のリスクはありません。


Imho、Objective Cのコードを見せて、そこでエラーを処理する方法を示した方がいいでしょう。人々はあなたが尋ねているのとは違う何かについて話しているようです。
Gangnus、2012

ある意味では、例外を使用する理由を探していると思います。私はそれらがどのように実装されるかについていくつかの考えを持っているので、私はそれらがどれほど高価になることができるかを知っています。しかし、私は彼らが使用するのに十分な議論を得ることができれば私はそのように行くと思います。
Johanssonによる

C ++の場合は、それらを使用ないように正当化する必要があるようです。ここの反応によると。
Gangnus

2
おそらく、しかしこれまでのところ、それらが優れている理由を説明している人はいません(エラーコードと比較した場合を除く)。例外的でないものに例外を使用するという概念は好きではありませんが、事実に基づくよりも本能的です。
Johanssonによる

「私は好きではない...例外的でないものに例外を使用する」-同意。
Gangnus

回答:


1

C ++は例外をより頻繁に使用することを好むようです。

C ++標準ライブラリは、その最も一般的なケースの設計形式(operator[]つまり、)でのランダムアクセスシーケンスの範囲外アクセスのようなプログラマエラーを通常スローしないため、実際にはいくつかの点でObjective-Cよりも少ないことをお勧めします。無効なイテレータを逆参照しようとしています。この言語は、範囲外の配列へのアクセス、nullポインターの逆参照など、このようなことをスローしません。

プログラマーのミスを例外処理の方程式から大きく外すと、実際には、他の言語がしばしば対応する非常に大きなカテゴリのエラーが取り除かれthrowingます。assertこのような場合、C ++は(リリース/本番ビルドではコンパイルされず、デバッグビルドでのみコンパイルされます)またはグリッチアウト(多くの場合クラッシュ)する傾向があります。これは、おそらく言語がそのようなランタイムチェックのコストを課したくないためです。プログラマーが自分でこのようなチェックを実行するコードを作成することによって特にコストを支払うことを望まない限り、そのようなプログラマーの間違いを検出するために必要とされるように。

Sutterは、C ++コーディング標準でのこのような場合の例外を回避することも推奨しています。

例外を使用してプログラミングエラーを報告する場合の主な欠点は、違反が検出された正確な行でデバッガーを起動するときに、スタックの巻き戻しを発生させたくない場合です。つまり、発生する可能性があることがわかっているエラーがあります(アイテム69から75を参照)。すべきでない他のすべてのこと、そしてそれが可能である場合、それはプログラマの責任ですassert

そのルールは必ずしも決まったものではありません。いくつかのよりミッションクリティカルなケースでは、たとえば、ラッパーと、プログラマーのミスが発生した場所を均一に記録するコーディング標準を使用することや、プログラマーのミスがthrow存在する場合に無効なものを参照したり、境界外にアクセスしたりすることが望ましい場合があります。このような場合、ソフトウェアに機会があれば、回復に失敗するにはコストがかかりすぎる可能性があります。しかし全体として、言語のより一般的な使用法は、プログラマーのミスに直面しないことを支持する傾向があります。

外部例外

プログラム外の外部ソースでの予期しない結果のように、C ++で最も頻繁に推奨される例外(標準委員会によると)が「外部例外」に対するものであると私が思う場合。たとえば、メモリの割り当てに失敗しています。もう1つは、ソフトウェアの実行に必要な重要なファイルを開けないことです。もう1つは、必要なサーバーへの接続に失敗しています。もう1つは、ユーザーが中止ボタンを押して操作をキャンセルし、一般的なケースの実行パスで、この外部割り込みがなければ成功することを期待している場合です。これらすべてのものは、直接のソフトウェアとそれを書いたプログラマの制御の外にあります。これらは、外部のソースからの予期しない結果であり、操作(実際には私の本では分割できないトランザクション*と見なされます)が成功することを妨げています。

取引

tryトランザクションは全体として成功するか、全体として失敗する必要があるため、ブロックを「トランザクション」と見なすことをお勧めします。何かを実行しようとして途中で失敗した場合、トランザクションがまったく実行されなかったかのように、プログラムの状態に加えられた副作用や変更をロールバックして、システムを有効な状態に戻す必要があります。クエリの途中で処理に失敗したRDBMSがデータベースの整合性を損なうべきではないのと同じです。上記のトランザクションでプログラム状態を直接変更する場合は、エラーが発生したときにプログラム状態を「変更解除」する必要があります(ここでスコープガードはRAIIで役立ちます)。

より簡単な代替策は、元のプログラムの状態を変更しないことです。そのコピーを変更し、それが成功した場合は、コピーを元のものと交換することができます(スワップがスローされないことを保証します)。失敗した場合は、コピーを破棄してください。これは、一般的にエラー処理に例外を使用しない場合にも当てはまります。エラーが発生する前にプログラム状態の変更が発生した場合、「トランザクション」の考え方が適切な回復の鍵となります。それは全体として成功するか、全体として失敗します。それはその突然変異を作ることの途中で成功しません。

これは奇妙なことに、エラーや例外処理を適切に行う方法についてプログラマーが尋ねるときに、あまり頻繁に議論されないトピックの1つですが、多くのプログラムの状態を直接変更したいソフトウェアで正しく処理するのが最も難しいのです。その操作。純粋性と不変性は、発生しない突然変異/外部の副作用をロールバックする必要がないため、スレッドセーフと同様に例外安全を実現するのに役立ちます。

パフォーマンス

例外を使用するかどうかのもう1つの指針となる要素はパフォーマンスです。私は、いくつかの強迫的でペニーピンチで逆効果的な方法を意味するわけではありません。多くのC ++コンパイラは、「ゼロコスト例外処理」と呼ばれるものを実装しています。

Cの戻り値のエラー処理よりも優れた、エラーのない実行のためのランタイムオーバーヘッドはありません。トレードオフとして、例外の伝播には大きなオーバーヘッドがあります。

私がそれについて読んだことによると、例外パスへのコストを大幅に歪める代わりに、一般的なケースの実行パスでオーバーヘッド(通常はCスタイルのエラーコードの処理と伝播に伴うオーバーヘッドさえも)が不要になります(つまりthrowing、今ではこれまで以上に高価です)。

「高価」は数量化が少し難しいですが、まず第一に、きついループで何百万回も投げたくないでしょう。この種の設計は、例外が常に左右に発生するとは限らないことを前提としています。

エラーなし

そして、そのパフォーマンスポイントにより、エラーが発生しなくなります。これは、他のあらゆる種類の言語を見ると、驚くほどあいまいです。ただし、上記のゼロコストEH設計を考えるとthrow、セット内にキーが見つからないことに応答して、ほとんどの場合、そのようなことはしたくないと思います。それは間違いなくエラーではない(キーを検索する人がセットを作成していて、常に存在するとは限らないキーを検索することを期待している可能性がある)だけでなく、そのコンテキストでは莫大なコストがかかるためです。

たとえば、集合交差関数は2つの集合をループして、それらが共通に持つキーを検索する場合があります。キーの検索に失敗した場合threw、ループが繰り返され、反復の半分以上で例外が発生する可能性があります。

Set<int> set_intersection(const Set<int>& a, const Set<int>& b)
{
     Set<int> intersection;
     for (int key: a)
     {
          try
          {
              b.find(key);
              intersection.insert(other_key);
          }
          catch (const KeyNotFoundException&)
          {
              // Do nothing.
          }
     }
     return intersection;
}

上記の例はまったくばかげて誇張されていますが、実稼働コードでは、C ++で例外を使用して他の言語から来ている人がいるのを見たことがあります。 C ++。上記の別のヒントは、catchブロックにはまったく何もすることがなく、そのような例外を強制的に無視するように書かれているだけであり、通常、C ++では例外があまり適切に使用されていないというヒント(保証人ではありません)です。

これらのタイプのケースでは、失敗を示すあるタイプの戻り値(false無効なイテレータに戻ること、またはnullptrコンテキストで意味のあるもの)が通常より適切であり、エラー以外のタイプのケースは通常、類似のcatchサイトに到達するために、いくつかのスタックの巻き戻しプロセスを必要としません。

ご質問

例外を回避する場合は、内部エラーフラグを使用する必要があります。それは扱いが面倒すぎますか、それとも例外よりもうまく機能しますか?両方のケースを比較するのが最良の答えです。

C ++で完全に例外を回避することは、組み込みシステムや、それらの使用を禁止する特定のタイプのケース(この場合、すべてを回避するために邪魔になる必要がある)で作業している場合を除いて、私にとって非常に逆効果です。throw厳密に使用する場合と同様に、ライブラリと言語の機能nothrow new

何らかの理由で例外を絶対に回避する必要がある場合(例:C APIをエクスポートするモジュールのC API境界を越えて作業する)、多くの人が私に同意しない場合がありますが、実際にはOpenGLのようなグローバルエラーハンドラー/ステータスをで使用することをお勧めしglGetError()ます。スレッドローカルストレージを使用して、スレッドごとに一意のエラーステータスを設定できます。

その理由は、残念ながらエラーコードが返されたときに、運用環境のチームが考えられるすべてのエラーを徹底的にチェックするのに慣れていないということです。それらが完全である場合、一部のC APIはほぼすべてのC API呼び出しでエラーが発生する可能性があり、完全なチェックには次のようなものが必要になります。

if ((err = ApiCall(...)) != success)
{
     // Handle error
}

...このようなチェックを必要とするAPIを呼び出すコードのほぼすべての1行で。それでも、私はチームと一緒に徹底的に作業する幸運はありませんでした。彼らはしばしばそのようなエラーを半分、時にはほとんどの場合、無視します。それが私にとって例外の最大の魅力です。このAPIをラップthrowして、エラーが発生したときにそれを統一すると、例外はおそらく無視できなくなり、私の見解と経験では、例外の優位性はそこにあります。

ただし、例外を使用できない場合、グローバルなスレッドごとのエラーステータスには、以前よりも少し前のエラーをキャッチできる可能性があるという利点があります(エラーコードを私に返すのに比べて非常に大きい)。ずさんなコードベースで発生し、それを完全に見逃して、何が起こったのか完全に気付かれないままにしていました。エラーは数行前または前の関数呼び出しで発生した可能性がありますが、ソフトウェアがまだクラッシュしていない場合は、逆方向に作業を開始し、発生場所と理由を特定できる可能性があります。

ポインタはまれであるため、例外を回避することを選択した場合は、内部エラーフラグを使用する必要があります。

ポインタがまれであるとは限りません。現在、C ++ 11以降には、コンテナーの基になるデータポインターと新しいnullptrキーワードを取得するメソッドもあります。例外が存在する場合にRAIIに準拠することがどれほど重要であるかを考えると、代わりに次のようなものを使用できる場合、メモリ所有または管理するための生のポインタを使用することは一般的に賢明ではないと考えられていunique_ptrます。しかし、メモリを所有/管理しない生のポインタは必ずしも(SutterやStroustrupのような人々からでも)それほど悪いとは見なされず、(物を指すインデックスと共に)物を指す方法として非常に実用的である場合があります。

それらは、無効にされた後に逆参照しようとした場合に検出されない標準のコンテナイテレータ(少なくともリリースでは、チェックされたイテレータがない)よりも間違いなく安全です。C ++は、特定の用途ですべてをラップし、所有していない生のポインターでさえも隠したくない場合を除いて、恥ずかしくないほど少し危険な言語です。リソースがRAIIに準拠していることを除いてほとんど重要です(通常、ランタイムコストはかかりません)。ただし、開発者が明示的に必要としないコストを回避するために、必ずしも最も安全な言語を使用しようとしているわけではありません。他のものと交換します。推奨される使用法は、ぶら下がりポインターや無効化されたイテレーターなどからユーザーを保護しようとすることではありません(そうでなければ、使用することをお勧めしますshared_ptrStroustrupが激しく反対しているあらゆる場所)。何かが発生したときに、リソースを適切に解放/解放/破棄/ロック解除/クリーンアップできないことからユーザーを保護しようとしていますthrows


14

C ++のユニークな歴史と柔軟性により、あなたが望む機能について事実上どんな意見でも宣言する人を見つけることができます。ただし、一般的には、実行していることがCのように見えるほど、考えが悪くなります。

例外に関してC ++の方がはるかに緩い理由の1つは、return nil好きなときにいつでも正確にできないということです。nil大多数のケースやタイプのようなものはありません。

しかし、ここに簡単な事実があります。例外は自動的に機能します。例外をスローすると、RAIIが引き継ぎ、すべてがコンパイラーによって処理されます。エラーコードを使用するときは、必ずチェックする必要があります。これは本質的に例外をエラーコードよりもはるかに安全にします。さらに、彼らはより表現力があります。例外をスローすると、エラーが何であるかを示す文字列を取得できます。また、「ZではなくYの値を持つ不正なパラメータX」などの特定の情報を含めることもできます。エラーコード0xDEADBEEFを取得し、正確には何が問題になっていますか?私は確かにドキュメントが最新の完全であると思います、そしてあなたが「不正パラメータ」を取得しても、それはあなたに言うつもりはありませんですましたパラメータ、どのような値であったか、どのような値であったか。参照でキャッチする場合も同様に、ポリモーフィックになる可能性があります。最後に、例外は、コンストラクターのように、エラーコードが決して実行できない場所からスローされる可能性があります。そして、一般的なアルゴリズムはどうですか?std::for_eachエラーコードをどのように処理しますか?プロのヒント:そうではありません。

例外は、あらゆる点でエラーコードよりもはるかに優れています。本当の問題は、例外対主張です。

つまりね。プログラムが動作するための事前条件、異常な異常条件、事前に確認できるバグ、バグなどを事前に知ることはできません。これは一般的に、プログラムロジックを知らなければ、特定の障害をアサーションにするか例外にするかを事前に決定できないことを意味します。また、サブ操作の1つが失敗したときに続行できる操作は例外であり、ルールではありません。

私の意見では、例外は捕らえられるべきものです。すぐにではなく、いつかは。例外は、ある時点でプログラムが回復できると予想される問題です。ただし、問題の操作は、例外が必要な問題から回復することはできません。

アサーションエラーは常に致命的で回復不可能なエラーです。メモリの破損、そのようなもの。

それで、ファイルを開くことができない場合、それはアサーションまたは例外ですか?まあ、一般的なライブラリでは、構成ファイルの読み込みなど、エラー処理できるシナリオがたくさんあります。代わりに、あらかじめ構築されたデフォルトを使用するだけなので、例外と言います。

脚注として、 "Null Object Pattern"の問題があることについて触れておきます。このパターンはひどいです。10年後には次のシングルトンになります。適切なnullオブジェクトを生成できるケースの数はごくわずかです。


代替案は必ずしも単純なintではありません。Obj-Cで使用していたNSErrorに似たものを使用する可能性が高くなります。
Johanssonによる

彼はCではなくObjective Cと比較しています。これは大きな違いです。そして、彼のエラーコードは行を説明しています。あなたが言うアルは正しいですが、どういうわけかこの質問に対する答えはありません。犯罪は意味しません。
Gangnus

もちろん、Objective-Cを使用している人なら誰でも、あなたは絶対に完全に間違っていると言うでしょう。
gnasher729

5

例外は、すべてのコードが次のようにならないようにするという理由で発明されました。

bool success = function1(&result1, &err);
if (!success) {
    error_handler(err);
    return;
}

success = function2(&result2, &err);
if (!success) {
    error_handler(err);
    return;
}

代わりに、次のようなものを取得し、1つの例外ハンドラーを上に移動するmainか、そうでなければ便利に配置します。

result1 = function1();
result2 = function2();

一部の人々は、例外のないアプローチのパフォーマンスの利点を主張しますが、私の意見では、可読性の懸念は、特にすべてのif (!success)ボイラープレートの実行時間を含める場合、どこにでも散布する必要があるか、そうしないとセグフォールトのデバッグが困難になるリスクを上回りますこれを含め、例外が発生する可能性を考慮することは比較的まれです。

なぜAppleが例外の使用を推奨しないのかはわかりません。彼らが未処理の例外の伝播を回避しようとしている場合、実際に達成することは、代わりにnullポインターを使用して例外を示すことです。そのため、プログラマーのミスは、はるかに有用なファイルが見つからない例外などではなく、nullポインター例外を引き起こします。


1
例外のないコードが実際にそのように見える場合、これはより理にかなっていますが、通常はそうではありません。このパターンは、error_handlerが戻らない場合に表示されますが、それ以外の場合はほとんど表示されません。
Johanssonによる

1

あなたが参照している投稿(例外とエラーコード)では、微妙に異なる議論が行われていると思います。問題は、#defineエラーコードのグローバルリストがあるかどうかで、ERR001_FILE_NOT_WRITEABLE(または、運が悪ければ省略)のような名前を付けてください。そして、そのスレッドの主なポイントは、多インスタンス言語でプログラミングする場合、オブジェクトインスタンスを使用する場合、そのような手続き型の構成は必要ないということです。例外は、単にその種類によって発生しているエラーを表すことができ、どのメッセージを出力するか(およびその他の情報も)のような情報をカプセル化できます。したがって、オブジェクト指向言語で手続き型にプログラミングする必要があるかどうかについては、その会話を1つとして捉えます。

ただし、コードで発生する状況を処理するために例外にいつどの程度依存するかについての会話は異なります。例外がスローされてキャッチされると、従来のコールスタックとはまったく異なる制御フローパラダイムが導入されます。例外のスローとは、基本的にはgotoステートメントであり、呼び出しスタックからユーザーを解放し、(クライアントコードが例外を処理することを決定する場所にある)不確定な場所にユーザーを送ります。これにより、例外ロジックがについて推論するのが非常に困難になります。

したがって、これらは最小化されるべきであるというジェリコによって表現されたような感情があります。つまり、ディスク上に存在する場合と存在しない場合があるファイルを読み取っているとします。ジェリコとアップル、そして私自身もそうであるように考える人たちは、例外をスローすることは適切ではないと主張するでしょう-そのファイルが存在しないことは例外ではないと予想されます。例外は通常の制御フローの代わりにはなりません。ReadFile()メソッドにブール値などを返させ、クライアントコードに戻り値falseからファイルが見つからなかったことを確認させるのも同じくらい簡単です。クライアントコードは、ユーザーファイルが見つからなかったことを通知したり、静かにそのケースを処理したり、何をしたいかを処理したりできます。

例外をスローすると、クライアントに負担がかかります。あなたは彼らに、通常のコールスタックからそれらを時々レンチして、彼らのアプリケーションがクラッシュするのを防ぐために彼らに追加​​のコードを書かせるコードを与えています。そのような強力で不快な負担は、実行時に絶対に必要な場合、または「早期に失敗する」という哲学のために、それらに強制されるべきです。したがって、前者の場合、アプリケーションの目的がネットワーク操作を監視することであり、誰かがネットワークケーブルを抜いた場合(致命的な障害を通知している場合)は、例外をスローできます。後者の場合、メソッドがnull以外のパラメーターを予期し、nullが渡された場合に例外をスローする可能性があります(不適切に使用されていることを通知しています)。

オブジェクト指向の世界で例外の代わりに何をするかについては、手続き型エラーコード構成以外のオプションがあります。すぐに思い浮かぶのは、OperationResultなどのオブジェクトを作成できることです。メソッドはOperationResultを返すことができ、そのオブジェクトは、操作が成功したかどうかに関する情報、および必要なその他の情報(ステータスメッセージなどですが、さらに微妙に、エラー回復の戦略をカプセル化できます) )。例外をスローする代わりにこれを返すと、ポリモーフィズムが可能になり、制御フローが保持され、デバッグがはるかに簡単になります。


私はあなたに同意しますが、それが私が質問をするように思わなかった理由のほとんどです。
Johanssonによる

0

Clean Codeには素敵な章がいくつかありますが、これはまさにこの主題についての話です-Appleの見解を除いて。

基本的な考え方は、関数からnilを返すことは決してないということです。

  • 多くの重複したコードを追加します
  • コードをめちゃくちゃにする-すなわち:読みにくくなる
  • 特定のプログラミング「宗教」に応じて、nil-pointerエラーまたはアクセス違反が発生することがよくあります。

他の付随するアイデアは、コードの混乱をさらに減らすのに役立ち、一連のエラーコードを作成する必要がなく、例外を使用することです。これにより、(特に複数のモジュールやプラットフォーム全体で)追跡および管理が最終的に困難になります。顧客が解読するのは面倒です。代わりに、問題の正確な性質を特定する意味のあるメッセージを作成し、デバッグを単純化し、エラー処理コードを基本的なtry..catchステートメントのような単純なものに減らすことができます。

COM開発の時代には、すべての失敗が例外を発生させるプロジェクトがあり、各例外は拡張Windows標準COM例外に基づく一意のエラーコードIDを使用する必要がありました。これは、以前にエラーコードが使用されていた以前のプロジェクトからの引き継ぎでしたが、会社はすべてをオブジェクト指向にして、やりすぎの方法を変更せずにCOMバンドワゴンにジャンプしたいと考えていました。彼らがエラーコード番号を使い尽くすのに4か月しかかからず、例外を使用するように大量のコードをリファクタリングすることに私は落ちました。事前に労力を節約し、例外を慎重に使用することをお勧めします。


ありがとう、しかし私はNULLを返すことを考えていません。とにかく、コンストラクターの前では不可能のようです。前述のように、オブジェクトではエラーフラグを使用する必要があります。
Johanssonによる
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.