すべてのブロックを「try」-「catch」でラップしない方がよいのはなぜですか?


430

メソッドが例外をスローする可能性がある場合、意味のあるtryブロックでこの呼び出しを保護しないのは無謀だと私は常に信じてきました。

私は投稿したばかりです ' try、catchブロックをスローする可能性のある呼び出しは常にラップする必要があります。この質問に対して、それは「著しく悪いアドバイス」であると言われました-理由を理解したいと思います。


4
メソッドが例外をスローすることがわかっている場合は、最初にそれを修正していました。これは、コードが例外をスローする場所と理由がわからないため、ソースでそれらをキャッチします(各メソッド-1つの汎用try-catch)。たとえば、チームから、以前のデータベースを使用したコードベースが提供されました。SQLレイヤーにtry catchを追加した後にのみ、多くの列が欠落してキャッチされました。第二に、オフラインでデバッグするために、メソッド名とメッセージをログに記録できます。それ以外の場合は、それがどこから発生したのかわかりません。
チャクラ

回答:


340

メソッドは、何らかの賢明な方法で処理できる場合にのみ、例外をキャッチする必要があります。

それ以外の場合は、呼び出しスタックの上位にあるメソッドがそれを理解できることを期待して、それを渡します。

他の人が述べたように、致命的なエラーが確実にログに記録されるように、コールスタックの最高レベルに未処理の例外ハンドラー(ログ付き)を置くことをお勧めします。


12
また、tryブロックには(生成されたコードの観点から)コストがかかることにも注意してください。スコット・マイヤーズの「より効果的なC ++」には良い議論があります。
Nick Meyer

28
実際のところtry、最新のCコンパイラではブロックは無料です。その情報はニックの日付です。また、局所性情報(命令が失敗した実際の場所)を失うため、トップレベルの例外ハンドラーを持つことにも同意しません。
ブリンディ2010

31
@Blindly:最上位の例外ハンドラーは例外を処理するためにそこにありませんが、実際には未処理の例外があったことを大声で叫び、そのメッセージを与え、適切な方法でプログラムを終了します(への呼び出しの代わりに1を返すterminate) 。それはより安全なメカニズムです。また、try/catch例外がなければ、多かれ少なかれ無料です。伝播が1つある場合、それがスローおよびキャッチされるたびに時間が消費されるため、try/catchその再スローのみのチェーンはコストがかからないわけではありません。
Matthieu

17
キャッチされない例外で常にクラッシュするべきではないことに同意します。最新のソフトウェア設計は非常に区分されているので、1つのエラーがあっただけで、アプリケーションの残りの部分(さらに重要なのはユーザー!)を罰する必要があるのはなぜですか。絶対に最後にやりたいことをクラッシュさせてください。少なくとも、アプリケーションの残りの部分にアクセスできない場合でも、作業を保存できる小さなコードウィンドウをユーザーに提供するようにしてください。
Kendall Helmstetter Gelner、2010

21
ケンドール:例外がトップレベルのハンドラーに到達した場合、アプリケーションは本来、未定義の状態にあります。特定のケースでは、ユーザーのデータを保持する価値があるかもしれませんが(Wordのドキュメント回復が頭に浮かびます)、プログラムはファイルを上書きしたり、データベースにコミットしたりすべきではありません。
ヒューブラケット

136

ミッチ 他の人が述べたように、あなたは、いくつかの方法で処理する予定がないという例外をキャッチしてはいけません。アプリケーションを設計するときは、アプリケーションが体系的に例外を処理する方法を検討する必要があります。これは通常、抽象化に基づいてエラー処理のレイヤーを持つことにつながります。たとえば、データアクセスコード内のすべてのSQL関連エラーを処理して、ドメインオブジェクトと対話するアプリケーションの部分がそこにあるという事実にさらされないようにします。どこかに隠れているDBです。

「どこでもすべてを捕まえる」臭いに加えて、絶対に避けたいいくつかの関連するコード臭があります。

  1. "catch、log、rethrow":スコープベースのロギングが必要な場合は、例外が原因でスタックがアンロールされるときに、デストラクタでログステートメントを発行するクラスを記述します(ala std::uncaught_exception())。必要なのは、関心のあるスコープでログ記録インスタンスを宣言することだけです。これで、ログ記録が作成され、不要なtry/ catchロジックはなくなりました。

  2. 「キャッチ、スロートランスレート」:これは通常、抽象化の問題を示します。いくつかの特定の例外をもう1つの一般的な例外に変換するフェデレーションソリューションを実装していない限り、不必要な抽象化レイヤーがある可能性があります。

  3. "catch、cleanup、rethrow":これは私のピートピーチの一つです。これがたくさんある場合は、リソース取得は初期化の手法を適用し、クリーンアップ部分を管理人オブジェクトインスタンスのデストラクタに配置する必要があります。

try/ catchブロックが散らばっているコードは、コードのレビューとリファクタリングの良いターゲットだと思います。これは、例外処理が十分に理解されていないか、コードがamabaになり、リファクタリングの深刻な必要性があることを示しています。


6
#1は初めてです。+1。また、#2の一般的な例外に注意したいと思います。これは、ライブラリを設計している場合、内部例外をライブラリインターフェイスで指定されたものに変換してカップリングを減らす必要があることを意味します(これは、 「フェデレーションソリューション」ですが、私はその用語に精通していません)。
rmeador 2010

3
基本的にあなたが言ったこと:parashift.com/c++
lite

1
#2はコード臭ではありませんが意味がありますが、古い例外をネストされた例外として保持することで拡張できます。
重複排除機能

1
#1に関して:std :: uncaught_exception()は、処理中にキャッチされない例外があることを通知しますが、AFAIKでは、catch()句のみが実際にその例外が何であるかを判断できます。したがって、キャッチされない例外が原因でスコープを終了しているという事実をログに記録できますが、囲んでいるtry / catchだけが詳細をログに記録できます。正しい?
Jeremy

@ジェレミー-あなたは正しいです。通常、例外を処理するときに例外の詳細をログに記録します。介在するフレームのトレースがあると非常に便利です。通常、ログ行を関連付けるために、スレッド識別子または特定のコンテキストをログに記録する必要があります。すべてのログ行にスレッドIDが含まれるLoggerクラスと同様のクラスを使用しlog4j.Logger、例外がアクティブなときにデストラクタで警告を発しました。
D.Shawley

48

次の質問は「例外をキャッチしたので、次に何をすればよいですか?」あなたは何をしますか?あなたが何もしない場合-それはエラーを隠すことであり、プログラムは何が起こったのかを見つける機会がなくても「うまくいかない」かもしれません。例外をキャッチしたら何をするかを正確に理解し、知っている場合にのみキャッチする必要があります。


29

すべてのブロックをtry-catchesでカバーする必要はありません。try-catchは、コールスタックのさらに下の関数でスローされた未処理の例外をキャッチできるためです。したがって、すべての関数にtry-catchを持たせるのではなく、アプリケーションの最上位ロジックに1つ持つことができます。たとえば、SaveDocument()他のメソッドなどを呼び出す多くのメソッドを呼び出すトップレベルのルーチンがあるかもしれません。これらのサブメソッドは、独自のtry-catchesを必要としません。なぜなら、それらがスローした場合でも、それはのSaveDocument()catch によってキャッチされるからです。

これは、3つの理由で便利ですSaveDocument()。エラーを報告する場所が1つしかないため、便利です。つまり、catchブロックです。すべてのサブメソッドでこれを繰り返す必要はありません。とにかくそれが必要なものです。問題が発生した場合の有用な診断を1つの場所でユーザーに提供します。

2つ目は、例外がスローされるたびに保存がキャンセルされることです。すべてのサブメソッドのtry-catchingで、例外がスローされた場合、そのメソッドのcatchブロックに入り、実行は関数を離れ、それからまで続きSaveDocument()ます。すでに何かがうまくいっていない場合は、すぐに停止したいと思うでしょう。

3つ目は、すべてのサブメソッドで、すべての呼び出しが成功したと想定できることです。呼び出しが失敗した場合、実行はcatchブロックにジャンプし、後続のコードは実行されません。これにより、コードがよりクリーンになります。たとえば、エラーコードは次のとおりです。

int ret = SaveFirstSection();

if (ret == FAILED)
{
    /* some diagnostic */
    return;
}

ret = SaveSecondSection();

if (ret == FAILED)
{
    /* some diagnostic */
    return;
}

ret = SaveThirdSection();

if (ret == FAILED)
{
    /* some diagnostic */
    return;
}

例外を除いて、次のように記述できます。

// these throw if failed, caught in SaveDocument's catch
SaveFirstSection();
SaveSecondSection();
SaveThirdSection();

何が起こっているのかがより明確になりました。

例外安全なコードは、他の方法で書くのが難しい場合があることに注意してください。例外がスローされた場合にメモリをリークしたくない場合。オブジェクトは常に例外の前に破棄されるため、RAII、STLコンテナ、スマートポインタ、およびデストラクタでリソースを解放するその他のオブジェクトについて知っておく必要があります。


2
素晴らしい例。ええ、ロード/保存などの「トランザクション」操作の周りなど、論理単位で可能な限り高くキャッチします。反復的で冗長なコードを付けたコードよりも悪くはありませんtry- catch少し異なるメッセージで少しずつ異なるエラーの順列にフラグを立てようとするブロックですが、実際にはすべて同じように終了します:トランザクションまたはプログラムの失敗と終了!例外に値する障害が発生した場合、ほとんどのユーザーはできる限りの救済をしたい、または少なくとも、それについて10レベルのメッセージを処理する必要なく放っておいてほしいと思っています。
underscore_d

これは私が今まで読んだ中で最高の「早めに、遅めにキャッチ」の説明の1つだと言いたかっただけです。簡潔で、例はあなたのポイントを完全に示しています。ありがとうございました!
corderazo00

27

ハーブサッターはこの問題についてここに書いています。確かに読む価値があります。
ティーザー:

「例外セーフなコードを書くことは、基本的に正しい場所に「try」と「catch」を書くことに関するものです。」話し合います。

率直に言って、そのステートメントは例外的な安全性の根本的な誤解を反映しています。例外はエラー報告のもう1つの形式であり、エラーセーフコードを書くことは、戻りコードをチェックしてエラー条件を処理する場所だけではないことは確かです。

実際には、例外の安全性は「試行」と「キャッチ」を書くことについてはめったにないことがわかり、そしてよりまれに優れていることがわかりました。また、例外の安全性がコードの設計に影響することを忘れないでください。まるで調味料のように、いくつかの追加のキャッチステートメントで後付けできる単なる後付けではありません。


15

他の回答で述べられているように、例外をキャッチできるのは、それに対して何らかの賢明なエラー処理を実行できる場合のみです。

たとえば、質問を生み出した質問で、質問者はlexical_cast整数から文字列への例外を無視しても安全かどうかを尋ねます。このようなキャストは決して失敗しないはずです。それが失敗した場合は、プログラムで何かがひどく間違っています。その状況で回復するために何ができるでしょうか?プログラムは信頼できない状態にあるため、プログラムを停止させるのがおそらく最善です。したがって、例外を処理しないのが最も安全な方法です。


12

例外をスローする可能性のあるメソッドの呼び出し元で常に例外をすぐに処理する場合、例外は役に立たなくなり、エラーコードを使用するほうがよいでしょう。

例外の要点は、呼び出しチェーンのすべてのメソッドで処理する必要がないことです。


9

私が聞いた最良のアドバイスは、例外条件について賢明に何かを行うことができるポイントでのみ例外をキャッチすべきであり、「キャッチ、ログ、およびリリース」は良い戦略ではないということです(ライブラリで時々避けられない場合)。


2
@KeithB:次善の策だと思います。別の方法でログを書き込むことができれば、さらに良いでしょう。
David Thornley、2010

1
@KeithB:これは、「ライブラリの何よりも優れている」戦略です。可能であれば、「キャッチ、ログ、適切に対処する」のが良いでしょう。(ええ、私はそれが常に可能であるとは限らないことを知っています。)
ドナルフェロー

6

できるだけ低いレベルでできるだけ多くの例外を処理するという質問の基本的な方向性に同意します。

既存の回答の一部は、「例外を処理する必要はありません。他の誰かがスタックで処理します」のようになります。考えないの悪い言い訳である私の経験に、現在開発されているコードでの例外処理を、例外処理を他の誰かまたはそれ以降の人の問題にしてしまうのです。

この問題は、同僚が実装したメソッドを呼び出す必要がある分散開発で劇的に増大します。次に、メソッド呼び出しのネストされたチェーンを検査して、彼/彼女が例外をスローしている理由を見つける必要があります。これは、最も深いネストされたメソッドではるかに簡単に処理できたはずです。


5

私のコンピュータサイエンスの教授がかつて私に与えたアドバイスは、「標準的な方法ではエラーを処理できない場合にのみ、TryブロックとCatchブロックを使用すること」でした。

例として、次のようなことを行うことができない場所でプログラムが深刻な問題に遭遇した場合、彼は私たちに言った:

int f()
{
    // Do stuff

    if (condition == false)
        return -1;
    return 0;
}

int condition = f();

if (f != 0)
{
    // handle error
}

次に、try、catchブロックを使用する必要があります。例外を使用してこれを処理することはできますが、例外はパフォーマンスの面でコストがかかるため、一般的にはお勧めできません。


7
これは1つの戦略ですが、関数からエラーコードや失敗/成功のステータスを返さず、代わりに例外を使用することをお勧めします。例外ベースのエラー処理は、多くの場合、エラーコードベースのコードよりも読みやすくなっています。(例については、この質問に対するAshleysBrainの回答を参照してください。)また、多くのコンピューターサイエンスの教授は、実際のコードを記述した経験がほとんどないことを常に覚えておいてください。
クリストファージョンソン

1
-1 @Sagelikaあなたの答えは例外を回避することにありますので、トライキャッチの必要はありません。
Vicente Botet Escriba 2010

3
@Kristopher:戻りコードの他の大きな欠点は、戻りコードを確認するのを忘れることが本当に簡単であり、呼び出しの直後が必ずしも問題を処理するのに最適な場所ではないということです。
David Thornley、2010

ええ、それは場合によって異なりますが、多くの場合(実際にスローしてはいけないときにスローする人を脇に置いて)、例外は非常に多くの理由でリターンコードよりも優れています。で、ほとんどの場合、例外が性能に有害であるという考えは、[要出典]大きいol」です
underscore_d

3

すべての関数の結果をテストする場合は、戻りコードを使用してください。

例外の目的は、少ない結果を頻繁にテストできるようにすることです。アイデアは、例外的な(珍しい、まれな)条件をより一般的なコードから分離することです。これにより、通常のコードがよりクリーンでシンプルに保たれますが、それらの例外的な条件も処理できます。

適切に設計されたコードでは、より深い関数がスローされ、より高い関数がキャッチする可能性があります。しかし重要なのは、「中間」にある多くの機能が例外的な条件を処理する負担から解放されることです。彼らは「例外安全」でなければならないだけで、それは彼らが捕まえなければならないという意味ではありません。


2

この議論に付け加えておきたいのは、C ++ 11以降、すべてのcatchブロックrethrowが、処理できる/すべきところまで例外である限り、それは非常に理にかなっているということです。これにより、バックトレースを生成できます。したがって、以前の意見の一部は古くなっていると思います。

使用std::nested_exceptionしてstd::throw_with_nested

StackOverflowの上で説明され、ここここにこれを達成する方法。

派生した例外クラスでこれを行うことができるため、そのようなバックトレースに多くの情報を追加できます!GitHubで私のMWEを確認することもできます。バックトレースは次のようになります。

Library API: Exception caught in function 'api_function'
Backtrace:
~/Git/mwe-cpp-exception/src/detail/Library.cpp:17 : library_function failed
~/Git/mwe-cpp-exception/src/detail/Library.cpp:13 : could not open file "nonexistent.txt"

2

アプリにエラーが多すぎてユーザーが問題と回避策にうんざりしていたため、いくつかのプロジェクトを救うための「機会」が与えられ、エグゼクティブは開発チーム全体を置き換えました。これらのコードベースはすべて、トップ投票の回答が説明するように、アプリレベルで集中型のエラー処理を備えていました。その答えがベストプラクティスである場合、それが機能せず、以前の開発チームが問題を解決できなかったのはなぜですか?おそらくそれは時々動作しませんか?上記の回答では、開発者が単一の問題の修正に費やす時間については触れていません。問題を解決する時間が重要な指標である場合、try..catchブロックを使用してコードをインストルメント化することをお勧めします。

私のチームはUIを大幅に変更せずに問題をどのように修正しましたか?単純です。すべてのメソッドはtry..catchでブロックされ、すべてが失敗した時点でメソッド名、メソッドパラメータ値がエラーメッセージ、エラーメッセージ、アプリ名、日付とともに渡される文字列に連結されてログに記録されました。とバージョン。この情報を使用して、開発者はエラーに対して分析を実行し、最も発生する例外を特定できます。または、エラーの数が最も多いネームスペース。また、モジュールで発生したエラーが適切に処理され、複数の理由が原因ではないことも検証できます。

これのもう1つの利点は、開発者がエラーロギングメソッドに1つのブレークポイントを設定でき、1つのブレークポイントと「ステップアウト」デバッグボタンの1回のクリックで、実際のメソッドへのフルアクセスで失敗したメソッドにあることです。障害発生時のオブジェクト。イミディエイトウィンドウで簡単に利用できます。デバッグが非常に簡単になり、実行をメソッドの先頭にドラッグして戻し、問題を複製して正確な行を見つけることができます。一元化された例外処理により、開発者は例外を30秒で複製できますか?番号。

「メソッドは、それが何らかの賢明な方法で処理できる場合にのみ、例外をキャッチする必要があります。」これは、開発者がリリース前に発生する可能性のあるすべてのエラーを予測できるか、発生する可能性があることを意味します。これがトップレベルである場合、アプリの例外ハンドラーは必要なく、Elastic Searchとlogstashの市場はありません。

このアプローチにより、開発者は本番環境で断続的な問題を見つけて修正することもできます。本番環境でデバッガなしでデバッグしますか?または、電話をかけて、動揺しているユーザーからメールを受け取りますか?これにより、他の誰かに気付かれる前に問題を修正できます。問題を修正するために必要なすべてのものがそこにあるので、サポートにメール、IM、またはSlackする必要はありません。95%の問題を再現する必要はありません。

適切に機能させるには、名前空間/モジュール、クラス名、メソッド、入力、およびエラーメッセージをキャプチャしてデータベースに格納できる集中型ロギングと組み合わせる必要があります。これにより、どのメソッドが最も失敗するかをハイライト表示して集約できるため、最初に修正しました。

開発者は、catchブロックからスタックに例外をスローすることを選択する場合がありますが、このアプローチは、スローしない通常のコードよりも100倍遅くなります。ロギングによるキャッチアンドリリースが推奨されます。

この手法は、12 Devsが2年間開発したFortune 500企業で、ほとんどのユーザーが1時間ごとに失敗するアプリをすばやく安定させるために使用されました。この3000の異なる例外を使用して、4か月で識別、修正、テスト、および展開されました。これは平均して、4か月間平均15分ごとに修正されます。

コードのインストルメント化に必要なすべてを入力するのは楽しいことではないことに同意します。繰り返しのコードを見ない方がよいですが、各メソッドに4行のコードを追加することは、長期的には価値があります。


1
すべてのブロックをラップするのはやり過ぎのようです。コードがすぐに肥大化し、読みにくくなります。より高いレベルで例外からスタックトレースをログに記録すると、問題が発生した場所がわかります。これは通常、エラー自体と組み合わせると、続行するのに十分な情報になります。あなたがどこでそれが不十分であるかを知りたいと思います。他の人の経験を得ることができるように。
user441521 2017年

1
「例外は通常のコードよりも100倍から1000倍遅く、再スローすべきではありません」-そのステートメントは、ほとんどの最新のコンパイラーおよびハードウェアには当てはまりません。
ミッチウィート

それはやり過ぎのようで、少しタイプする必要がありますが、例外の分析を実行して、本番環境での断続的なエラーを含む最大のエラーを最初に見つけて修正する唯一の方法です。catchブロックは、必要に応じて特定のエラーを処理し、ログに記録するコードが1行あります。
user2502917

いいえ、例外は非常に遅いです。代替は、戻りコード、オブジェクト、または変数です。このスタックオーバーフローの記事を参照してください...「例外がリターンコードより遅い少なくとも30,000倍である」stackoverflow.com/questions/891217/...
user2502917

1

上記のアドバイスに加えて、個人的には、try + catch + throwを使用しています。次の理由により:

  1. 別のコーダーの境界で、他の人が書いた呼び出し元に例外がスローされる前に、自分で書いたコードでtry + catch + throwを使用します。これにより、コードでエラー条件が発生したことを知る機会があり、この場所は、最初に例外をスローするコードにはるかに近いほど、理由を見つけやすくなります。
  2. モジュールの境界で、異なるモジュールが私の同じ人に書かれるかもしれませんが。
  3. 学習+デバッグの目的、この場合、C ++ではcatch(...)を使用し、C#ではcatch(Exception ex)を使用します。C++の場合、標準ライブラリはあまり多くの例外をスローしないため、このケースはC ++ではまれです。しかし、C#の一般的な場所には、C#には巨大なライブラリと成熟した例外階層があり、C#ライブラリコードは大量の例外をスローします。理論的には、私(およびあなた)は、呼び出した関数からのすべての例外を知っており、理由/理由を知っているはずです。これらの例外はスローされ、それらを適切に処理する方法(バイパスするか、キャッチして適切に処理する方法)を知っています。残念ながら、実際には、1行のコードを記述する前に、潜在的な例外についてすべてを知ることは非常に困難です。だから私はすべてをキャッチし、実際に例外が発生したときにロギング(製品環境で)/ダイアログをアサート(開発環境で)することで私のコードに声を出して話させます。このようにして、例外処理コードを段階的に追加します。私はそれが良いアドバイスと矛盾していることを知っていますが、実際にはそれは私のために機能し、私はこの問題のためのより良い方法を知りません。

1

マイク・ウィートの回答は主なポイントをかなりうまくまとめていますが、私は別の回答を追加せざるを得ません。このように思います。複数のことを行うメソッドがある場合、複雑さを増加させるのではなく、追加することになります。

つまり、try catchにラップされたメソッドには、2つの結果が考えられます。あなたは非例外の結果と例外の結果を持っています。あなたが多くの方法を扱っているとき、これは理解を超えて指数関数的に爆破します。

なぜなら、各メソッドが2つの異なる方法で分岐する場合、別のメソッドを呼び出すたびに、潜在的な結果の以前の数を二乗しているからです。5つのメソッドを呼び出すまでに、最低256の結果が得られます。これと比較しない、すべての単一のメソッドでtry / catchを実行すると、たどるパスは1つだけです。

それが基本的に私がそれを見る方法です。どのタイプの分岐でも同じことを行うと主張したくなるかもしれませんが、アプリケーションの状態は基本的に未定義になるため、try / catchは特殊なケースです。

つまり、try / catchesを使用すると、コードを理解するのが非常に難しくなります。


-2

内部のコードのすべての部分を隠す必要はありませんtry-catchtry-catchブロックの主な用途は、エラー処理と、プログラム内のバグ/例外です。try-catch-の使用法

  1. このブロックは、例外を処理する場所で使用できます。または、単に、記述されたコードのブロックが例外をスローする可能性があると言うことができます。
  2. 使用後すぐにオブジェクトを破棄したい場合は、try-catchブロックを使用できます。

1
「使用後すぐにオブジェクトを破棄したい場合は、try-catchブロックを使用できます。」これは、RAII /最小限のオブジェクトの寿命を促進することを意図していましたか?もしそうなら、まあ、try/ catchはそれと完全に分離/直交しています。より小さなスコープでオブジェクトを破棄する場合は、単に新しいオブジェクトを開くことができます。もちろん、実際に何かを必要としない限り{ Block likeThis; /* <- that object is destroyed here -> */ }、これをtry/ にラップする必要はありません。catchcatch
underscore_d

#2-例外でオブジェクトを(手動で作成された)破棄するのは奇妙に思えますが、これは一部の言語で役立つ場合がありますが、通常はtry / finally "try / except block"内で行い、具体的にはexceptブロック自体-オブジェクト自体が最初に例外の原因になっている可能性があるため、別の例外が発生し、クラッシュする可能性があります。
TS
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.