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_ptr
Stroustrupが激しく反対しているあらゆる場所)。何かが発生したときに、リソースを適切に解放/解放/破棄/ロック解除/クリーンアップできないことからユーザーを保護しようとしていますthrows
。