例外の契約を作成および実施するにはどうすればよいですか?


33

私はチームのリーダーに、bool isSuccessfulまたはenumをエラーコードとともに返す代わりに、C ++で例外を使用できるように説得しようとしています。しかし、私は彼のこの批判に対抗することはできません。

このライブラリを検討してください。

class OpenFileException() : public std::runtime_error {
}

void B();
void C();

/** Does blah and blah. */
void B() {
    // The developer of B() either forgot to handle C()'s exception or
    // chooses not to handle it and let it go up the stack.
    C();
};

/** Does blah blah.
 *
 * @raise OpenFileException When we failed to open the file. */
void C() {
    throw new OpenFileException();
};
  1. 開発者がB()関数を呼び出すことを検討してください。彼はそのドキュメントをチェックし、例外を返さないことを確認したため、何もキャッチしようとしません。このコードは、本番環境でプログラムをクラッシュさせる可能性があります。

  2. 開発者がC()関数を呼び出すことを検討してください。彼はドキュメントをチェックしないので、例外をキャッチしません。この呼び出しは安全ではなく、本番環境でプログラムをクラッシュさせる可能性があります。

しかし、この方法でエラーをチェックする場合:

void old_C(myenum &return_code);

その関数を使用している開発者は、その引数を提供しない場合、コンパイラーによって警告され、「あ、これは確認する必要があるエラーコードを返します」と言うでしょう。

ある種の契約があるように、例外を安全に使用するにはどうすればよいですか?


4
@Sjoerd主な利点は、開発者がコンパイラーによって関数に変数を提供して、リターンコードを保存することを余儀なくされることです。エラーが発生する可能性があることを認識しており、エラーを処理する必要があります。これは、コンパイル時のチェックをまったく行わない例外よりもはるかに優れています。
DBedrenko

4
「彼はそれを処理する必要があります」-これが起こるかどうかのチェックもありません。
シェード

4
@Sjoerdそれは必要ではありません。開発者に、エラーが発生する可能性があり、エラーをチェックする必要があることを認識させるのに十分です。レビューアも、エラーをチェックしたかどうかにかかわらず、見た目で見ることができます。
DBedrenko

5
Either/ Result
monad

5
@gardenheadあまりにも多くの開発者がそれを適切に使用しないため、Becauseいコードになってしまい、自分ではなく機能を非難し続けます。Javaのチェックされた例外機能は美しいものですが、一部の人々は単にそれを取得しないため、悪いラップを取得します。
AxiomaticNexus

回答:


47

これは例外の正当な批判です。 多くの場合、コードを返すなどの単純なエラー処理ほど目に見えません。また、「契約」を実施する簡単な方法はありません。ポイントの一部は、より高いレベルで例外をキャッチできるようにすることです(すべてのレベルですべての例外をキャッチする必要がある場合、とにかくエラーコードを返すこととの違いは何ですか?)。そして、これは、適切に処理しない他のコードによってコードが呼び出される可能性があることを意味します。

例外には欠点があります。費用対効果に基づいてケースを作成する必要があります。
:私は役立つこれら二つの記事見つかった例外の必要性例外を除いて、すべての間違ったを。 また、このブログ投稿では、C ++に焦点を当てて、例外に関する多くの専門家の意見を提供しています。専門家の意見は例外を支持するように見えるが、明確なコンセンサスからはほど遠い。

チームリーダーを説得することに関しては、これは適切な戦いではないかもしれません。特にレガシーコードではそうではありません。上記の2番目のリンクで述べたように:

例外は、例外に対して安全でないコードを介して伝播することはできません。したがって、例外の使用は、プロジェクト内のすべてのコードが例外に対して安全でなければならないことを意味します。

主に使用しないプロジェクトに例外を使用するコードを少し追加しても、おそらく改善にはなりません。それ以外の点で適切に記述されたコードで例外を使用しないことは、壊滅的な問題にはほど遠いです。アプリケーションやあなたが尋ねる専門家によっては、まったく問題にならないかもしれません。あなたの戦いを選択する必要があります。

これはおそらく、少なくとも新しいプロジェクトが開始されるまで、私が努力するであろう議論ではありません。そして、あなたが新しいプロジェクトを持っているとしても、それはレガシーコードによって使用されるのでしょうか、それとも使用されるのでしょうか?


1
アドバイスとリンクをありがとう。これは新しいプロジェクトであり、レガシープロジェクトではありません。例外の使用が安全ではないほど大きな欠点があるのに、なぜ例外が広範囲に広がっているのかと思っています。上位レベルの例外をキャッチし、後でコードを修正して移動した場合、例外がまだキャッチされて処理されることを保証する静的チェックはありません)。
DBedrenko

3
@Deeは、例外を支持するいくつかの引数について上記のリンクを参照します(より専門的な意見を持つリンクを追加しました)。

6
この答えはスポットです!
ドックブラウン

1
私は経験から、既存のコードベースに例外を追加しようとすることは本当に悪い考えだと述べることができます。私はそのようなコードベースで作業してきましたが、何があなたの顔に爆発するのか分からなかったので、作業するのは本当に大変でした。例外は、事前に計画するプロジェクトでのみ使用してください。
マイケルコーン

26

この主題について書かれた文字通り全部の本があるので、どんな答えでもせいぜい要約になります。ここに、あなたの質問に基づいて、価値があると思う重要なポイントをいくつか示します。完全なリストではありません。


例外はあちこちでキャッチされることを意図していません。

メインループに一般的な例外ハンドラがある限り-アプリケーションの種類(Webサーバー、ローカルサービス、コマンドラインユーティリティなど)に応じて-通常、必要なすべての例外ハンドラがあります。

私のコードでは、メインループの外側には、catchステートメントが少ししかありません(存在する場合でも)。そして、それが現代のC ++の一般的なアプローチのようです。


例外と戻りコードは相互に排他的ではありません。

これをオールオアナッシングのアプローチにしないでください。例外は、例外的な状況に使用する必要があります。「構成ファイルが見つかりません」、「ディスクがいっぱい」など、ローカルで処理できないもの。

ユーザーが指定したファイル名が有効かどうかを確認するなどの一般的な失敗は、例外のユースケースではありません。そのような場合は、代わりに戻り値を使用してください。

上記の例からわかるように、「ファイルが見つかりません」は、ユースケースに応じて、例外またはリターンコードのいずれかです。

したがって、絶対的なルールはありません。大まかなガイドラインは次のとおりです。ローカルで処理できる場合、戻り値にします。ローカルで処理できない場合は、例外をスローします。


例外の静的チェックは役に立ちません。

とにかく例外はローカルで処理されないため、通常、どの例外をスローできるかは重要ではありません。唯一の有用な情報は、例外をスローできるかどうかです。

Javaには静的チェックがありますが、通常は失敗した実験と見なされ、ほとんどの言語(特にC#)にはそのような静的チェックがありません。これは、 C#にそれがない理由についての良い読み物です。

これらの理由により、C ++は推奨されなくなりましthrow(exceptionA, exceptionB)noexcept(true)。デフォルトでは、関数はスローできるため、プログラマーは、ドキュメントで明示的に約束されていない限り、それを期待する必要があります。


例外セーフコードの記述は、例外ハンドラの記述とは関係ありません。

私はむしろ、例外安全なコードを書くことは、例外ハンドラを書くことを避ける方法についてすべてであると言いたいです!

ほとんどのベストプラクティスは、例外ハンドラの数を減らすことを目的としています。コードを1回書いて自動的に呼び出す(たとえばRAIIを使用する)と、同じコードをコピーアンドペーストするよりもバグが少なくなります。


3
一般的な例外ハンドラがある限り...あなたは通常すでに大丈夫です。」これは例外安全なコードを書くことは非常に難しいことを強調するほとんどのソースと矛盾します。

12
@ dan1111「例外セーフコードの作成」は、例外ハンドラの作成とはほとんど関係ありません。例外セーフコードの記述とは、RAIIを使用してリソースをカプセル化し、「最初にすべてローカルで作業してからスワップ/移動を使用する」などのベストプラクティスを使用することです。あなたはそれらに慣れていないとき、それらは挑戦的です。ただし、「例外ハンドラの作成」は、これらのベストプラクティスの一部ではありません。
シェード

4
@AxiomaticNexusあるレベルでは、機能の失敗した使用法をすべて「悪い開発者」として説明できます。最大のアイデアは、正しいことを簡単に行えるため、不適切な使用減らすものです。静的にチェックされた例外は、そのカテゴリに該当しません。多くの場合、記述されているコードがそれらを気にする必要さえない場所で、価値に釣り合わない作業を作成します。ユーザーは、コンパイラに邪魔をさせないように間違った方法でそれらを黙らせようとします。正しく行われたとしても、多くの混乱を招きます。
jpmc26

4
@AxiomaticNexusそれが不釣り合いな理由は、これがコードベース全体のほぼすべての関数に容易に伝播できるからです。クラスの半分のすべての関数がデータベースにアクセスする場合、すべての関数が明示的に宣言する必要があるわけではなく、SQLExceptionをスローできます。また、それらを呼び出すすべてのコントローラーメソッドも同様です。作業量がどれほど少ないかに関係なく、そのくらいのノイズは実際には負の値です。Sjoerdは頭に釘を打ちます。コードの大部分は、例外について何もできないので、例外を気にする必要はありません
jpmc26

3
@AxiomaticNexusはい、最高の開発者は非常に完璧であるため、コードは常にすべての可能なユーザー入力を完全に処理し、それらの例外は決して prodに表示されません。もしそうなら、それはあなたが人生の失敗であることを意味します。真剣に、このゴミを私に与えないでください。完璧な人はいませんし、最高の人でもありません。また、「正気」が「停止して半分まともなエラーページ/メッセージを返す」場合でも、これらのエラーが発生しても、アプリは正常に動作するはずです。そして、それを推測してください。これは、99%のIOExceptions でも同じことです。チェックされた区分は任意であり、役に立たない。
jpmc26

8

C ++プログラマーは例外仕様を探しません。彼らは例外保証を探します。

コードの一部が例外をスローしたと仮定します。プログラマーはどのような仮定を立てることができますか?コードの記述方法から、例外の余波でコードは何を保証しますか?

または、特定のコードがスローしないことを保証できる可能性はありますか(つまり、OSプロセスが終了すること以外に何もありません)?

「ロールバック」という言葉は、例外に関する議論で頻繁に使用されます。有効な状態(明示的に文書化されている)にロールバックできることは、例外保証の例です。例外の保証がない場合、プログラムはその場で終了するはずです。それは、その後実行するコードが意図したとおりに動作することさえ保証されていないためです。たとえば、メモリが破損した場合、それ以降の操作は技術的に未定義の動作です。

さまざまなC ++プログラミング手法が例外保証を促進します。RAII(スコープベースのリソース管理)は、クリーンアップコードを実行し、通常のケースと例外的なケースの両方でリソースが確実に解放されるメカニズムを提供します。オブジェクトの変更を実行する前にデータのコピーを作成すると、操作が失敗した場合にオブジェクトの状態を復元できます。等々。

このStackOverflowの質問への回答は、C ++プログラマーがコードに発生する可能性のあるすべての障害モードを理解し、障害にもかかわらずプログラム状態の有効性を保護しようとする長さを垣間見せます。C ++コードの行ごとの分析が習慣になります。

C ++で開発する場合(本番用)、詳細を詳しく説明する余裕はありません。また、バイナリblob(非オープンソース)はC ++プログラマの悩みの種です。バイナリBLOBを呼び出さなければならず、BLOBが失敗した場合、C ++プログラマーは次にリバースエンジニアリングを行います。

参照:http : //en.cppreference.com/w/cpp/language/exceptions#Exception_safety-例外の安全性を参照してください。

C ++は、例外仕様の実装に失敗しました。他の言語でのその後の分析では、例外仕様は単に実用的ではないと述べています。

試行が失敗した理由:厳密に強制するには、型システムの一部である必要があります。しかし、そうではありません。コンパイラーは、例外仕様をチェックしません。

C ++がそれを選択した理由、および他の言語(Java)の経験が例外仕様が重要でないことを証明する理由:関数の実装を変更すると(たとえば、新しい種類の例外をスローする可能性のある別の関数を呼び出す必要がある場合)例外)、厳密な例外仕様の施行は、その仕様も更新する必要があることを意味します。これは伝播します-単純な変更であるため、数十または数百の関数の例外仕様を更新しなければならない場合があります。抽象基底クラス(インターフェースのC ++同等物)の状況は悪化しています。インターフェースで例外仕様が実施されている場合、インターフェースの実装は、異なるタイプの例外をスローする関数を呼び出すことができません。

参照:http : //www.gotw.ca/publications/mill22.htm

C ++ 17以降、[[nodiscard]]属性は関数の戻り値で使用できます(https://stackoverflow.com/questions/39327028/can-ac-function-be-declared-such-that-the-return-valueを参照)-無視できません)。

それで、コードを変更して、新しい種類の障害条件(つまり、新しいタイプの例外)が発生した場合、それは重大な変更ですか?呼び出し元にコードの更新を強いる必要があったのでしょうか、それとも少なくとも変更について警告されるべきですか?

C ++プログラマーが例外仕様ではなく例外保証を探す引数を受け入れた場合、答えは、新しい種類の障害条件がコードの以前の約束を保証する例外のいずれかを壊さない場合、それは重大な変更ではないということです。


「関数の実装を変更すると(たとえば、新しい種類の例外をスローする可能性のある別の関数を呼び出す必要がある場合)、厳密な例外仕様の施行は、その仕様も更新する必要があることを意味します」 、必要に応じて。代替手段は何でしょうか?新しく導入された例外を無視しますか?
AxiomaticNexus

@AxiomaticNexusそれは例外保証によっても答えられます。実際、仕様が完全になることは決してないと主張します。保証は私たちが探しているものです。多くの場合、どの保証がまだ有効であったかを推測するために、どの保証が破られているかを調べるために、例外タイプを比較します。例外仕様には、チェックする必要のあるタイプの一部がリストされていますが、実際のシステムでは、リストを完全に仕上げることはできないことに注意してください。ほとんどの場合、例外タイプを「食べる」というショートカットを
ユーザーに強制

@AxiomaticNexus例外の使用については賛成でも反対でもありません。典型的な例外の使用法は、基本例外クラスといくつかの派生例外クラスを持つことを推奨します。一方、他の例外タイプ、たとえばC ++ STLまたは他のライブラリからスローされた例外が可能であることを覚えておく必要があります。この場合、例外仕様を機能させることができると主張できます。標準例外(std::exception)および独自のベース例外がスローされることを指定します。しかし、プロジェクト全体に適用した場合、すべての機能が同じ仕様、つまりノイズを持つことになります。
rwong

例外に関しては、C ++とJava / C#は異なる言語であることを指摘しておきます。第一に、C ++は任意のコードの実行やデータの上書きを許可するという意味でタイプセーフではありません。第二に、C ++は優れたスタックトレースを出力しません。これらの違いにより、C ++プログラマはエラーと例外処理の点で異なる選択を余儀なくされました。また、特定のプロジェクトがC ++が適切な言語ではないと正当に主張できることも意味します。(たとえば、個人的には、TIFF画像のデコードをCまたはC ++で実装することは許可されないと考えています。)
rwong

1
@rwong:C ++モデルに従う言語の例外処理の大きな問題は、多くの場合、重要なのは例外の直接的な原因ではなく、それが意味するものである場合、例外オブジェクトのタイプcatch基づいていることですシステム状態。残念ながら、例外のタイプは一般に、オブジェクトが部分的に更新された(したがって無効な)状態のままになるような方法でコードが破壊的に終了するかどうかについては何も述べていません。
supercat

4

C()関数を呼び出す開発者を検討してください。彼はドキュメントをチェックしないので、例外をキャッチしません。呼び出しは安全ではありません

それは完全に安全です。彼はすべての場所で例外をキャッチする必要はありません。実際に何か役に立つことができる場所でtry / catchを投げることができます。スレッドからのリークを許可した場合にのみ安全ではありませんが、通常、それを防ぐことは難しくありません。


彼は例外がから出てくることを期待C()していないので安全ではなく、その結果、スキップされた呼び出しに続くコードを保護しません。何らかの不変性が真のままであることを保証するためにそのコードが必要な場合、その保証は、から出てくる最初の例外でパフになりC()ます。完全に例外に対して安全なソフトウェアのようなものはなく、例外が決して予想されなかった場所ではないことは間違いありません。
cmaster

1
@cmaster Himは、例外が発生することを期待していないことC()は大きな間違いです。C ++のデフォルトでは、すべての関数がスローできnoexcept(true)ます。そうでない場合は、コンパイラーに明示的に指示する必要があります。この約束がない場合、プログラマーは関数がスローできると想定しなければなりません。
シェード

1
@cmasterそれは彼自身の愚かな誤りであり、C()の作者とは何の関係もありません。例外の安全性は重要であり、彼はそれについて学び、それに従うべきです。彼がnoexceptであることがわからない関数を呼び出す場合、それがスローされた場合に何が起こるかをうまく処理する必要があります。noexceptが必要な場合は、実装を見つける必要があります。
-DeadMG

@SjoerdおよびDeadMG:標準では、すべての関数が例外をスローすることを許可していますが、それはプロジェクトがコード内で例外を許可する必要があるという意味ではありません。OPのチームリーダーがやっているように、コードから例外を禁止する場合は、例外セーフプログラミングを行う必要はありません。また、以前は例外がなかったコードベースへの例外を許可するようにチームリーダーを説得しようとすると、例外が存在するとC()機能が途切れることがあります。これは実際には非常に賢明なことではなく、多くの問題につながることが運命づけられています。
cmaster

@cmasterこれは新しいプロジェクトに関するものです-受け入れられた回答に関する最初のコメントを参照してください。したがって、既存の関数がないため、壊れる既存の関数はありません。
シェード

3

重要なシステムを構築する場合は、チームリーダーのアドバイスに従うことを検討し、例外を使用しないでください。これは、ロッキードマーティンのジョイントストライクファイター航空車両C ++コーディング標準の AVルール208 です。一方、MISRA C ++ガイドラインには、MISRA準拠のソフトウェアシステムを構築している場合に例外を使用できる場合と使用できない場合に関する非常に具体的なルールがあります。

重要なシステムを構築している場合、静的分析ツールも実行している可能性があります。多くの静的分析ツールは、メソッドの戻り値をチェックしないと警告を表示し、エラー処理が欠落している場合をすぐに明らかにします。私の知る限り、適切な例外処理を検出するための同様のツールサポートはそれほど強力ではありません。

究極的には、静的解析と組み合わせた契約および防御プログラミングによる設計は、例外よりも重要なソフトウェアシステムにとって安全であると主張します。

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