web-dev-qa-db-ja.com

C ++での例外の慣用的な使用法

isocpp.orgの例外に関するFAQ 状態

関数の使用中にコーディングエラーを示すために、throwを使用しないでください。プロセスをデバッガーに送信するか、プロセスをクラッシュさせ、開発者がデバッグするためにクラッシュダンプを収集するには、assertまたは他のメカニズムを使用します。

一方、標準ライブラリはstd :: logic_errorとその派生物をすべて定義しているため、プログラミングエラーを処理することになっているようです。空の文字列をstd :: stof(invalid_argumentがスローされます)に渡しても、プログラミングエラーではありませんか? 「1」/「0」以外の文字を含む文字列をstd :: bitset(invalid_argumentをスローします)に渡しても、プログラミングエラーではありませんか?無効なインデックスを使用してstd :: bitset :: setを呼び出しても(out_of_rangeがスローされます)、プログラミングエラーではありませんか?そうでない場合、テストするプログラミングエラーは何ですか? std :: bitset文字列ベースのコンストラクタはC++ 11以降にのみ存在するため、例外の慣用的な使用を考慮して設計されている必要があります。一方、logic_errorは基本的にまったく使用しないでくださいと言われたことがあります。

例外が頻繁に発生する別のルールは、「例外的な状況でのみ例外を使用する」です。しかし、ライブラリ関数はどのような状況が例外的であるかを知ることになっているのでしょうか?一部のプログラムでは、ファイルを開くことができないことは例外的な場合があります。他の人にとって、メモリを割り当てることができないことは例外ではないかもしれません。そしてその中間には数百のケースがあります。ソケットを作成できませんか?接続できない、またはソケットまたはファイルにデータを書き込むことができない?入力を解析できませんか?例外かもしれませんが、そうではないかもしれません。関数自体は、一般に確実に知ることはできません。どのようなコンテキストで呼び出されているかはわかりません。

それで、特定の関数に例外を使用するかどうかをどのように決定するべきですか?実際に一貫している唯一の方法は、それらをすべてのエラー処理に使用するか、何も使用しないことです。そして、標準ライブラリを使用している場合、その選択は私のためになされました。

16
cooky451

まず、_std::exception_とその子はずっと前に設計されたことを指摘しなければなりません。今日設計されている場合、おそらく(ほぼ確実に)異なると思われる部品がいくつかあります。

誤解しないでください。かなりうまくいった設計の部分があり、C++の例外階層を設計する方法のかなり良い例です(たとえば、他のほとんどのクラスとは異なり、それらはすべて、共通ルート)。

特に_logic_error_を見ると、少し難問があります。一方で、問題に関して妥当な選択がある場合は、引用したアドバイスが正しいです。デバッグと修正ができるように、できるだけ速く、騒々しく失敗するのが一般的にです。

ただし、良くも悪くも、標準ライブラリを定義するのは困難です一般的にを実行する必要があります。誤った入力が与えられたときにプログラムを終了するようにこれらを定義した場合(たとえばabort()を呼び出す)、それはalwaysがその状況で発生したものであり、実際にはかなりの数があります少なくとも展開されたコードでは、これがおそらく正しいことではない状況です。

これは、(少なくともソフトな)リアルタイム要件があり、不正な出力に対するペナルティが最小限のコードに適用されます。たとえば、チャットプログラムについて考えてみましょう。音声データをデコードしていて、正しくない入力を受け取った場合、ユーザーは、完全にシャットダウンするプログラムよりも、ミリ秒の静的な出力を使用する方がはるかに幸せになる可能性があります。同様に、ビデオの再生を行う場合、入力ストリームが破損したためにプログラムを即座に終了させるよりも、フレームの一部のピクセルに誤った値を生成してしまう方が許容できる場合があります。

例外を使用して特定の種類のエラーを報告するかどうかについては、そのとおりです。使用方法によっては、同じ操作が例外として認められるかどうかが決まります。

一方、あなたも間違っています-標準ライブラリを使用しても、(必ずしも)その決定を強制することはありません。ファイルを開く場合、通常はiostreamを使用します。 Iostreamは正確に最新で最高のデザインでもありませんが、この場合は問題がありません。エラーモードを設定できるので、ファイルのオープンに失敗して例外がスローされるかどうかを制御できます。したがって、アプリケーションにとって本当に必要であるファイルがあり、それを開けない場合は、深刻な是正措置を講じる必要があり、それを開けない場合に例外をスローさせることができます。ファイル。ほとんどのファイルは、開こうとしますが、存在しないかアクセスできない場合は、失敗します(これがデフォルトです)。

どのように決めるかについては、簡単な答えはないと思います。良くも悪くも、「例外的な状況」は測定が必ずしも容易ではありません。決定が容易なケースは確かに例外的ではありませんが、疑問を抱きやすいケースや、手元の関数のドメイン外にあるコンテキストの知識が必要なケースもあります(おそらく常にそうなるでしょう)。そのような場合、iostreamのこの部分とほぼ同様の設計を検討する価値があります。この場合、ユーザーは失敗の結果、例外がスローされるかどうかを判断できます。あるいは、2つの別個の関数セット(またはクラスなど)を使用することも完全に可能です。1つは失敗を示す例外をスローし、もう1つは他の手段を使用します。そのルートをたどると、一方が他方のラッパーになるか、両方が実際の作業の根幹を実装する関数の共有セットのラッパーとして機能する可能性がかなり高くなります。

15
Jerry Coffin

Std :: bitset文字列ベースのコンストラクタはC++ 11以降にのみ存在するため、例外の慣用的な使用を考慮して設計されている必要があります。一方、logic_errorは基本的にまったく使用しないでくださいと言われたことがあります。

あなたはこれを信じていないかもしれませんが、まあ、異なるC++プログラマーは同意しません。 FAQが1つのことを言っているのはそのためですが、標準ライブラリは同意しません。

FAQは、デバッグが容易になるため、クラッシュを推奨します。クラッシュしてコアダンプを取得すると、アプリケーションの正確な状態がわかります。例外をスローすると、その状態の多く。

標準ライブラリは、コーダーにエラーをキャッチして処理する能力を与えることがデバッグ性よりも重要であるという理論を採用しています。

例外かもしれませんが、そうではないかもしれません。関数自体は、一般に確実に知ることはできません。どのようなコンテキストで呼び出されているかはわかりません。

ここでの考え方は、関数が状況が例外的であるかどうかを知らない場合、例外をスローすべきではないということです。他のメカニズムを介してエラー状態を返す必要があります。状態が例外的であることがわかっているプログラム内のポイントに到達すると、例外をスローする必要があります。

しかし、これには独自の問題があります。関数からエラー状態が返された場合、その状態を確認するのを忘れている可能性があり、エラーは通知なしで渡されます。これは、一部の人々が例外を放棄することにつながり、あらゆる種類のエラー状態に対して例外をスローすることを支持する例外的な規則です。

全体として重要な点は、例外をスローするタイミングについては、人によって考え方が異なるということです。あなたは単一のまとまりのあるアイデアを見つけるつもりはありません。一部の人々は、これが例外を処理する正しい方法であると独断的に主張しますが、単一の合意された理論はありません。

例外をスローすることができます:

  1. 決して
  2. どこにでも
  3. プログラマエラーのみ
  4. プログラマーのエラーはありません
  5. 非日常的な(例外的な)障害時のみ

あなたに同意するインターネット上の誰かを見つけてください。自分に合ったスタイルを採用する必要があります。

9
Winston Ewert

他にも多くの良い答えが書かれていますが、簡単な説明を付け加えたいと思います。

従来の答えは、特にISO C++ FAQが作成された場合、主に「C++例外」と「Cスタイルの戻りコード」を比較します。3番目のオプションは、「あるタイプの複合値を返す、たとえば、structまたはunion、または最近では、boost::variantまたは(提案されている)std::expectedは考慮されません。

C++ 11以前は、「複合型を返す」オプションは通常非常に弱かった。なぜなら、移動のセマンティクスがないため、構造の内外へのコピーは非常にコストがかかる可能性があるからです。最高のパフォーマンスを得るには、言語のその時点で、コードを [〜#〜] rvo [〜#〜] にスタイルすることが非常に重要でした。例外は、複合型を効果的に返す簡単な方法のようなものでしたが、それ以外の場合は非常に困難です。

IMO、C++ 11以降では、このオプションは、Rust最近使用されているイディオムResult<T, E>と同様に、C++コードでより頻繁に使用される必要があります。これは本当にエラーを示すよりシンプルで便利なスタイルです。例外を除いて、以前にスローしなかった関数がリファクタリング後に突然スローし始める可能性は常にあります。プログラマーは常にそのようなものをうまく文書化するわけではありません。識別された共用体の戻り値の一部としてエラーが示されると、Cスタイルのエラー処理に対する通常の批判である、プログラマーが単にエラーコードを無視する可能性が大幅に減少します。

通常Result<T, E>は、ブーストオプションのような働きをします。 operator boolを使用して、値またはエラーかどうかをテストできます。次に、say operator *を使用して値にアクセスするか、他の「取得」関数にアクセスします。通常、速度を上げるために、そのアクセスはチェックされていません。ただし、デバッグビルドでアクセスがチェックされ、アサーションによってエラーではなく値が実際にあることを確認できるようにすることもできます。このようにして、エラーを適切にチェックしない人は、より陰湿な問題ではなく、ハードアサートを取得します。

追加の利点は、キャッチされない場合、スタックが任意の距離を飛ぶだけの例外とは異なり、このスタイルでは、関数が以前にはなかった場所にエラーを通知し始めたときに、コンパイルしない限りコンパイルできないことです。コードを変更して処理します。これにより、問題が大きくなります。「キャッチされない例外」の従来の問題は、ランタイムエラーではなくコンパイル時エラーのようになります。

私はこのスタイルの大ファンになりました。通常、私は現在、これまたは例外のいずれかを使用しています。しかし、私は例外を主要な問題に限定しようとします。解析エラーなどの場合は、たとえばexpected<T>を返すようにします。 std::stoiboost::lexical_castのような、比較的マイナーな問題「文字列を数値に変換できませんでした」が発生した場合にC++例外をスローするものは、最近の私には非常に味が悪いようです。

2
Chris Beck

これはデザインの一部であるため、非常に主観的な問題です。そして、デザインは基本的にアートなので、私はこれらのことを議論するよりも議論することを好みます(私があなたが議論していると言っているのではありません)。

私にとって、例外的なケースは2種類あります-リソースを扱うケースと重要なオペレーションを扱うケースです。クリティカルと見なすことができるものは、目前の問題に依存し、多くの場合、プログラマーの視点に依存します。

リソースの取得の失敗は、例外をスローするための最有力候補です。リソースには、メモリ、ファイル、ネットワーク接続など、問題とプラットフォームに基づいたものを使用できます。さて、リソースを解放できなかった場合、例外は発生しますか?まあ、それは再び依存します。メモリの解放に失敗したところは何もしていませんので、そのシナリオについてはわかりません。ただし、リソースの解放の一環としてファイルを削除すると失敗する可能性があり、失敗しました。その失敗は通常、マルチプロセスアプリケーションで開いたままにしていた他のプロセスにリンクしています。ファイルのようにリリース中に他のリソースが失敗する可能性があると思います。通常、この問題を引き起こすのは設計上の欠陥であるため、例外をスローするよりも修正する方が良いでしょう。

次に、リソースを更新します。この点は、少なくとも私にとって、アプリケーションの重要な運用面に密接に関連しています。与えられたコンマ区切りの文字列に基づいて詳細を変更する関数UpdateDetails(std::string&)を持つEmployeeクラスを想像してください。メモリの解放が失敗するのと同様に、メンバー変数の値の割り当てが失敗することは、そのようなドメインでの経験がないために、失敗することを想像するのは難しいと思います。ただし、名前が示すように機能するUpdateDetailsAndUpdateFile(std::string&)のような関数は失敗することが予想されます。これは私がクリティカルオペレーションと呼んでいるものです。

ここで、いわゆるクリティカル操作で例外をスローする必要があるかどうかを確認する必要があります。つまり、ファイルの更新は、デストラクタのように最後に行われますか、それとも、すべての更新後に行われる偏執的な呼び出しですか?未書き込みのオブジェクトを定期的に書き込むフォールバックメカニズムはありますか?私が言っていることは、あなたは手術の重要性を評価しなければならないということです。

明らかに、リソースに関連付けられていない多くの重要な操作があります。 UpdateDetails()に誤ったデータが渡された場合、詳細は更新されず、失敗を通知する必要があるため、ここで例外をスローします。しかし、GiveRaise()のような関数を想像してみてください。さて、前述の従業員が先のとがった髪のボスを持っていて、昇給できない場合(プログラミング用語では、一部の変数の値によってこれが起こらない)、関数は本質的に失敗しています。ここで例外をスローしますか?私が言っているのは、例外の必要性を評価しなければならないということです。

私にとって、一貫性とは、クラスの使いやすさよりも、デザインアプローチの面で重要です。つまり、「すべてのGet関数はこれを行う必要があり、すべてのUpdate関数はこれを行う必要がある」とは考えていませんが、特定の関数が私のアプローチ内の特定のアイデアにアピールするかどうかを確認します。表面的には、クラスは「偶然」のように見えるかもしれませんが、ユーザー(ほとんどの場合、他のチームの同僚)がそれについて怒ったり質問したりするときはいつでも説明し、満足しているようです。

CではなくC++を使用しているために、基本的に戻り値を例外に置き換える人が多く、「エラー処理の適切な分離」などを提供し、「言語の混合」などをやめるように促しています。通常、そのような人々。

1
vin