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


16

isocpp.org例外よくある質問の状態

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

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

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

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


6
そのFAQエントリを注意深く読む必要があります。無効なデータ、nullオブジェクトの逆参照、または一般的な実行時の不具合に関係するものはなく、コーディングエラー にのみ適用されます。一般的に、アサーションとは、決して起こらないことを特定することです。 他のすべてについては、例外、エラーコードなどがあります。
ロバートハーベイ

1
@RobertHarveyは、その定義にはまだ同じ問題があります-何かが人間の介入なしで解決できるかどうかは、プログラムの上位層だけに知られています。
cooky451 16

1
あなたは法律学にこだわっています。長所と短所を評価し、自分の心を決めてください。また、あなたの質問の最後の段落...私はその自明性を全く考慮しません。あなたの思考は非常に白黒であり、真実はおそらくいくつかの灰色の濃淡に近いでしょう。
ロバートハーベイ

4
この質問をする前に調査を試みましたか?C ++のエラー処理イディオムは、Webで吐き気を催すような詳細でほぼ確実に説明されています。1つのFAQエントリへの1つの参照は、良い研究にはなりません。あなたが研究を行った後、あなたはまだあなた自身の決心をしなければなりません。私たちのプログラミング学校が、どうやって自分自身で考えるかわからない心のないソフトウェアパターンコーディングロボットをどのように作成しているのか、始めてはいけません。
ロバートハーベイ

2
そのようなルールは実際には存在しないかもしれないという私の理論に信Which性を与えます。The C ++ Loungeから何人かを招待して、彼らがあなたの質問に答えられるかどうかを確認しました。だからあなた自身の責任で彼らのアドバイスを取ります。
ロバートハーベイ

回答:


15

まず、私はそれを指摘する義務がstd::exceptionあり、その子供たちはずっと前に設計されたと感じています。今日設計されている場合、おそらく(ほぼ確実に)異なる多くの部品があります。

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

特にを見るlogic_errorと、少し難問があります。一方で、あなたが問題に合理的な選択がある場合、あなたが引用したアドバイスは正しいです。デバッグと修正ができるように、できるだけ速く、うるさく失敗することが一般的に最善です。

しかし、良くも悪くも、一般的にすべきことを中心に標準ライブラリを定義することは困難です。abort()不正な入力が与えられたときにプログラムを終了するようにこれらを定義した場合(たとえば、呼び出し)、それはその状況で常に発生することです。 、少なくともデプロイされたコードでは。

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

特定の種類のエラーを報告するために例外を使用するかどうかに関しては、あなたの言うとおりです。同じ操作でも、その使用方法に応じて、例外とみなされる場合があります。

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

あなたがどのように決めるかに関して:私は簡単な答えがあるとは思わない。良くも悪くも、「例外的な状況」を測定するのは必ずしも簡単ではありません。決定するのが簡単なケースは確かに例外的である必要がありますが、疑問を抱えている場合や、手元の関数の領域外のコンテキストの知識が必要な場合があります(おそらく常にそうなります)。そのような場合には、少なくとも、ユーザーが障害の結果として例外がスローされるかどうかを決定できる、iostreamのこの部分にほぼ類似した設計を検討する価値があります。あるいは、2つの別個の関数セット(またはクラスなど)を使用することもできます。一方は失敗を示す例外をスローし、他方は他の手段を使用します。そのルートに行くと、


9

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

あなたはこれを信じないかもしれませんが、まあ、異なるC ++コーダーは同意しません。そのため、FAQでは1つのことを述べていますが、標準ライブラリには同意していません。

よくある質問では、デバッグが簡単になるため、クラッシュを推奨しています。クラッシュしてコアダンプを取得した場合、アプリケーションの正確な状態がわかります。例外をスローすると、その状態の多くが失われます。

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

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

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

しかし、これには独自の問題があります。関数からエラー状態が返された場合、それをチェックすることを忘れてしまう可能性があり、エラーは黙って通過します。これにより、一部の人々は、例外があらゆる種類のエラー状態に対して例外をスローすることを支持する例外的なルールであることを放棄することになります。

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

例外をスローできます:

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

インターネット上であなたに同意する人を見つけます。あなたに合ったスタイルを採用する必要があります。


例外のパフォーマンスが低い言語について教えている人々は、本当に例外的な状況でのみ例外を使用するという提案が広く推進されていることに注意する価値があります。C ++はこれらの言語の1つではありません。
ジュール

1
@Jules-(パフォーマンス)があなたの主張を裏付ける独自の答えに値することは確かです。C ++例外のパフォーマンス確かに問題であり、他の場所よりも多分少ないかもしれませんが、「C ++はこれらの言語の1つではありません(例外のパフォーマンス低い場合)」と述べることは確かに議論の余地があります。
マーティンBa

1
@MartinBa-たとえばJavaと比較すると、C ++例外のパフォーマンスは桁違いに高速です。ベンチマークでは、例外を1レベル上にスローするパフォーマンスは、C ++での戻り値の処理よりもJavaでの1000倍以上遅いのに対し、約50倍遅いことが示唆されています。この場合のJava向けに書かれたアドバイスは、C ++に特に注意を払って適用するべきではありません。2つのパフォーマンスの差は1桁以上あるからです。おそらく、「パフォーマンスが悪い」というよりは、「パフォーマンスが極端に低い」と書くべきでした。
ジュール

1
@Jules-これらの数字に感謝します。(任意のソース?)私ができるのJava(およびC#)必要性は確かにスタックトレース、キャプチャするため、それらを信じているようだ、それは本当に高価になる可能性がありますように。私はまだ、あなたの最初の反応はちょっと誤解を招くと思います。C ++のようなパフォーマンス指向の言語で。
マーティンBa

2

他にも多くの良い答えが書かれていますが、私は短い点を付け加えたいだけです。

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

C ++ 11より前は、「複合型を返す」オプションは通常非常に脆弱でした。移動セマンティクスがなかったため、構造体の内外へのコピーは潜在的に非常に高価でした。最高のパフォーマンスを得るには、言語のその時点でRVOに向けてコードをスタイルすることが非常に重要でした。例外は、複合型を効果的に返す簡単な方法のようなものでした。

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

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

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

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


1
std::expectedまだ受け入れられていない提案ですか?
マーティンBa

あなたは正しい、私はそれがまだ受け入れられていないと思います。しかし、いくつかのオープンソース実装が浮かんできており、私は自分自身を数回ロールバックしました。可能な状態は2つしかないため、バリアント型を実行するよりも複雑ではありません。主な設計上の考慮事項は、あなたが望む正確なインターフェースのようなものであり、エラーオブジェクトが実際にあるはずのAndrescuのexpected <T>のexception_ptrようにしたいのですか、それとも何らかの構造タイプまたは何かを使用したいのですかそのような。
クリスベック

Andrei Alexandrescuの講演は次のとおりです。
クリスベック

提案された[[nodiscard]] attribute方法は、誤ってエラー結果を単純に無視しないようにするため、このエラー処理アプローチに役立ちます。
CodesInChaos

-はい、私はAAの話を知っていました。展開するために(except_ptr)内部で例外をスローする必要があるため、デザインがかなり奇妙であることがわかりました。個人的には、そのようなツールは完全に独立して動作するはずだと思います。ただの発言。
マーティンBa

1

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

私にとって、例外的なケースには2種類あります。リソースを扱うケースと、重要な操作を扱うケースです。クリティカルと見なされるものは、当面の問題に依存し、多くの場合、プログラマーの視点に依存します。

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

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

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

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

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

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


1

第一に、他の人が述べたように、物事はC ++ではそれほど明確ではありません主にIMHOは要件や制約がC ++で他の言語よりも多少異なるためです。「類似の」例外の問題があるC#とJava。

std :: stofの例で公開します。

プログラミングエラーではなく、空の文字列をstd :: stof(invalid_argumentをスローします)に渡す

私が見るように、この関数の基本的なコントラクトは、引数をフロートに変換しようとすることでありそうしないと例外によって報告されます。考えられる例外は両方とも派生してlogic_errorいますが、プログラマーエラーという意味ではなく、「入力を浮動小数点数に変換することはできません」という意味です。

ここで、logic_error(実行時)入力を考えると、a を使用して、変換しようとするのは常にエラーであることを示すことができます-しかし、それを判断して(例外を介して)伝えるのは関数の仕事です。

サイドノート:そのビューでは、関数への同じ入力が与えられると、異なる実行に対して理論的に成功するruntime_error 可能性があるものとしてaを見ることできます。(たとえば、ファイル操作、DBアクセスなど)

補足説明:C ++正規表現ライブラリ、ここと同じように分類できるruntime_error場合もありますが(無効な正規表現パターン)、エラー派生させることを選択しました

これは、C ++ではグループ化logic_またはruntime_エラーがかなり曖昧であり、一般的なケースではあまり役に立たないことを示しています(*)-特定のエラーを処理する必要がある場合は、おそらく2つ未満をキャッチする必要があります。

(*):それは、コードの一片が一貫してはならないと言うことではないのですが、あなたは投げるかどうruntime_logic_またはcustom_代重要なのは、私は考えていないということは本当にあります。


stofとの両方にコメントするにはbitset

両方の関数は引数として文字列を取ります。どちらの場合も次のとおりです。

  • 特定の文字列が有効かどうかを呼び出し側で確認するのは簡単です(たとえば、最悪の場合、関数ロジックを複製する必要があります。ビットセットの場合、空の文字列が有効かどうかはすぐにはわかりません。
  • 文字列を「解析」するのはすでに関数の責任であるため、すでに文字列を検証する必要があるため、文字列を均一に「使用」するためにエラーを報告することは理にかなっています(両方の場合、これは例外です) 。

例外が頻繁に発生するルールは、「例外的な状況でのみ例外を使用する」です。しかし、ライブラリ関数はどのような状況が例外的であるかをどのように認識するのでしょうか?

このステートメントには、2つのルートがあります。

パフォーマンス:クリティカルパスで関数が呼び出され、「例外」ケースが例外的でない場合、つまり、大量のパスに例外をスローする必要がある場合、例外を巻き戻す機械に毎回支払うことは意味がありません、遅すぎる可能性があります。

エラー処理の産地:関数が呼び出されると、例外がすぐにキャッチされて処理されている場合はエラーハンドリングがより冗長になるように、例外をスローに少しポイントがあるcatchしてよりif

例:

float readOrDefault;
try {
  readOrDefault = stof(...);
} catch(std::exception&) {
  // discard execption, just use default value
  readOrDefault = 3.14f; // 3.14 is the default value if cannot be read
}

TryParsevs.のParseような関数が機能する場所は次のとおりです。ローカルコードが解析された文字列が有効であることを期待する場合の1つのバージョン、ローカルコードが解析が失敗すると実際に予期される(つまり非例外)と想定する場合の1つのバージョン。

確かに、stofは(として定義されている)の周りのラッパーなstrtofので、例外が必要ない場合は、それを使用します。


だから、特定の機能に例外を使用するかどうかをどのように決定するのですか?

私見、あなたは2つのケースがあります:

  • 「ライブラリ」のような関数(異なるコンテキストで頻繁に再利用されます):基本的には決定できません。おそらく両方のバージョンを提供します。おそらく、エラーを報告するバージョンと、返されたエラーを例外に変換するラッパーバージョンです。

  • 「アプリケーション」関数(アプリケーションコードのblobに固有で、一部は再利用できますが、アプリのエラー処理スタイルなどによって制約されます):ここでは、多くの場合、かなり明確になります。関数を呼び出すコードパス適切かつ有用な方法で例外を処理する場合は、例外を使用してエラーを報告します(ただし、以下を参照)。エラーリターンスタイルのためにアプリケーションコードがより簡単に読み書きされる場合は、必ずそれを使用してください。

もちろん間には場所があります-必要なものを使用して、YAGNIを覚えておいてください。


最後に、よくある質問のステートメントに戻りますが、

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

何かがひどく台無しにされていること明確に示しているか、呼び出し元のコードがそれが何をしていたかを明確に知らなかったすべてのエラーについて、私はこれにサブスクライブします。

ただしこれが適切な場合は、多くの場合、アプリケーション固有です。したがって、上記のライブラリドメインとアプリケーションドメインを参照してください。

これは、呼び出し前提条件を検証するかどうかとその方法に関する質問に基づいていますが、私はそれには入りません。すでに長すぎると答えます:-)

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.