it-swarm-ja.tech

なぜ例外を保守的に使用する必要があるのですか?

例外はめったに使用されるべきではないと人々が言うのをよく見たり聞いたりしますが、その理由を説明することは決してありません。それは本当かもしれませんが、理論的根拠は通常glibです: "それは理由のために例外と呼ばれます"これは私には一種のようです立派なプログラマー/エンジニアが決して受け入れてはならない説明。

例外を使用して解決できる問題にはさまざまなものがあります。 制御フローにそれらを使用するのが賢明でないのはなぜですか?それらの使用方法を非常に保守的にする背後にある哲学は何ですか?セマンティクス?パフォーマンス?複雑さ?美学?コンベンション?

以前にパフォーマンスに関する分析を見たことがありますが、一部のシステムに関連し、他のシステムには関連しないレベルです。

繰り返しになりますが、特別な状況のためにそれらを保存する必要があることに必ずしも同意しませんが、コンセンサスの論理的根拠は何であるか疑問に思っています(そのようなものが存在する場合)。

78
Catskul

摩擦の主なポイントはセマンティクスです。多くの開発者は例外を悪用し、あらゆる機会にそれらをスローします。アイデアは、やや例外的な状況に対して例外を使用することです。たとえば、間違ったユーザー入力は例外としてカウントされません。これは、これが発生してその準備ができていることを期待しているためです。しかし、ファイルを作成しようとして、ディスクに十分なスペースがなかった場合は、そうです、これは明確な例外です。

もう1つの問題は、例外が頻繁にスローされて飲み込まれることです。開発者はこの手法を使用して、プログラムを単に「無音」にし、完全に崩壊するまで可能な限り長く実行させます。これは非常に間違っています。例外を処理しない場合、一部のリソースを解放して適切に対応しない場合、例外の発生をログに記録しない場合、または少なくともユーザーに通知しない場合は、その意味に例外を使用していません。

あなたの質問に直接答えます。例外的な状況はまれであり、例外は高額であるため、例外を使用することはめったにありません。

まれです。ボタンを押すたびに、または不正な形式のユーザー入力ごとにプログラムがクラッシュするとは思わないためです。たとえば、データベースに突然アクセスできなくなったり、ディスクに十分な容量がなかったり、依存しているサードパーティのサービスがオフラインになったりする場合があります。これはすべて発生する可能性がありますが、非常にまれですが、これらが明らかに例外的なケースです。

例外をスローすると通常のプログラムフローが中断されるため、費用がかかります。ランタイムは、例外を処理できる適切な例外ハンドラーが見つかるまで、スタックを巻き戻します。また、ハンドラーが受け取る例外オブジェクトに渡される途中で呼び出し情報を収集します。それはすべてコストがかかります。

これは、例外を使用することに例外がないということではありません(笑顔)。多くのレイヤーを介してリターンコードを転送する代わりに例外をスローすると、コード構造が単純化される場合があります。簡単なルールとして、あるメソッドが頻繁に呼び出され、半分の時間で「例外的な」状況を発見することが予想される場合は、別の解決策を見つけることをお勧めします。ただし、この「例外的な」状況が発生するのはごくまれな状況である場合でも、ほとんどの場合通常の操作の流れが予想される場合は、例外をスローするだけで問題ありません。

@Comments:例外は、コードをより単純で簡単にすることができれば、例外の少ない状況でも確実に使用できます。このオプションはオープンですが、実際には非常にまれだと思います。

制御フローにそれらを使用するのが賢明でないのはなぜですか?

例外は通常の「制御フロー」を混乱させるためです。例外が発生すると、プログラムの通常の実行が中止され、オブジェクトが一貫性のない状態になり、一部の開いているリソースが解放されない可能性があります。確かに、C#にはusingステートメントがあり、using本体から例外がスローされた場合でもオブジェクトが確実に破棄されます。しかし、とりあえず言語から抽象化しましょう。フレームワークがオブジェクトを破棄しないと仮定します。手動で行います。リソースとメモリを要求して解放する方法についてのシステムがあります。どのような状況でオブジェクトとリソースを解放する責任があるかについて、システム全体で合意があります。外部ライブラリの扱い方にはルールがあります。プログラムが通常の操作フローに従っている場合、これはうまく機能します。しかし、実行の途中で突然例外がスローされます。リソースの半分は解放されません。半分はまだ要求されていません。操作がトランザクションであることが意図されていた場合、それは壊れています。リソースの解放を担当するコード部分は単に実行されないため、リソースを処理するためのルールは機能しません。他の誰かがそれらのリソースを使用したい場合、彼らはこの特定の状況を予測できなかったため、それらが一貫性のない状態にあり、同様にクラッシュする可能性があります。

たとえば、メソッドM()がメソッドN()を呼び出して作業を行い、リソースを調整してからM()に戻す必要があるとします。それを使用してから廃棄します。結構です。 N()で問題が発生し、M()で予期していなかった例外がスローされるため、何らかのメソッドでキャッチされるまで例外が一番上に表示されますC() N()の奥深くで何が起こっていたのか、そしていくつかのリソースを解放するかどうか、そしてどのように解放するのかがわかりません。

例外をスローすることで、プログラムを、予測、理解、および処理するのが難しい多くの新しい予測不可能な中間状態にする方法を作成します。 GOTOを使用するのと少し似ています。ある場所から別の場所にランダムに実行をジャンプできるプログラムを設計することは非常に困難です。また、それを維持およびデバッグすることも困難になります。プログラムが複雑になると、それを修正するために、いつ、どこで何が起こっているのかについての概要が失われます。

90
user151323

「例外的な状況で例外をスローする」がglibの答えですが、実際にはそれらの状況を定義できます:前提条件は満たされているが、事後条件は満たされていません 。これにより、エラー処理を犠牲にすることなく、より厳密で、より厳密で、より有用な事後条件を記述できます。それ以外の場合は、例外なく、考えられるすべてのエラー状態を考慮して事後条件を変更する必要があります。

  • 前提条件は、関数を呼び出す前に真である必要があります
  • 事後条件は、関数がafteritreturnsを保証するものです。
  • 例外安全性は、例外が関数またはデータ構造の内部整合性にどのように影響するかを示し、多くの場合、外部から渡された動作(たとえば、ファンクター、テンプレートパラメーターのctorなど)を処理します。 。

コンストラクター

C++で記述できる可能性のあるすべてのクラスのすべてのコンストラクターについて言えることはほとんどありませんが、いくつかのことがあります。その中で最も重要なのは、構築されたオブジェクト(つまり、コンストラクターが戻ることで成功したオブジェクト)が破棄されることです。 言語はそれが真であると想定し、デストラクタを自動的に呼び出すため、この事後条件を変更することはできません。(技術的には、未定義の動作の可能性を受け入れることができます。言語はno何かについて保証しますが、それはおそらく他の場所でカバーされている方が良いでしょう。)

コンストラクターが成功できない場合に例外をスローする唯一の方法は、クラスの基本定義(「クラス不変」)を変更して、有効な「null」またはゾンビ状態を許可し、コンストラクターがゾンビを作成して「成功」できるようにすることです。 。

ゾンビの例

このゾンビの変更の例はstd :: ifstreamであり、使用する前に常にその状態を確認する必要があります。たとえば、std :: stringはそうではないため、構築後すぐに使用できることが常に保証されます。この例のようなコードを作成する必要があり、ゾンビの状態を確認するのを忘れた場合、サイレントに誤った結果が得られるか、プログラムの他の部分が破損することを想像してみてください。

string s = "abc";
if (s.memory_allocation_succeeded()) {
  do_something_with(s); // etc.
}

そのメソッドに名前を付けることでさえ、クラスの不変条件を変更する必要がある方法の良い例であり、状況stringはそれ自体を予測も処理もできません。

入力例の検証

一般的な例、つまりユーザー入力の検証について説明しましょう。入力の失敗を許可したいからといって、解析関数がそれを事後条件に含める必要があるという意味ではありません。ただし、ハンドラーがパーサーが失敗したかどうかを確認する必要があることを意味します。

// boost::lexical_cast<int>() is the parsing function here
void show_square() {
  using namespace std;
  assert(cin); // precondition for show_square()
  cout << "Enter a number: ";
  string line;
  if (!getline(cin, line)) { // EOF on cin
    // error handling omitted, that EOF will not be reached is considered
    // part of the precondition for this function for the sake of example
    //
    // note: the below Python version throws an EOFError from raw_input
    //  in this case, and handling this situation is the only difference
    //  between the two
  }
  int n;
  try {
    n = boost::lexical_cast<int>(line);
    // lexical_cast returns an int
    // if line == "abc", it obviously cannot meet that postcondition
  }
  catch (boost::bad_lexical_cast&) {
    cout << "I can't do that, Dave.\n";
    return;
  }
  cout << n * n << '\n';
}

残念ながら、これは、C++のスコープでRAII/SBRMを破る必要がある方法の2つの例を示しています。 Pythonの例では、その問題はなく、C++に必要なものが示されています– try-else:

# int() is the parsing "function" here
def show_square():
  line = raw_input("Enter a number: ") # same precondition as above
  # however, here raw_input will throw an exception instead of us
  # using assert
  try:
    n = int(line)
  except ValueError:
    print "I can't do that, Dave."
  else:
    print n * n

前提条件

前提条件を厳密にチェックする必要はありません。違反すると常に論理障害が発生し、呼び出し元の責任になります。ただし、前提条件をチェックする場合は、例外をスローするのが適切です。 (場合によっては、ガベージを返すか、プログラムをクラッシュさせる方が適切です。ただし、これらのアクションは他のコンテキストではひどく間違っている可能性があります。最適な処理方法 未定義の動作 は別のトピックです。)

特に、stdlib例外階層のstd :: logic_errorブランチとstd :: runtime_errorブランチを対比してください。前者は前提条件違反によく使用されますが、後者は事後条件違反に適しています。

61
Roger Pate
  1. 高価
    カーネル(システム)シグナルインターフェイスを管理するためのカーネル呼び出し(または他のシステムAPI呼び出し)
  2. 分析が難しい
    gotoステートメントの問題の多くは例外に当てはまります。多くの場合、複数のルーチンやソースファイルで大量のコードを飛び越えます。これは、中間ソースコードを読んでも必ずしも明らかではありません。 (Javaです。)
  3. 中間コードでは必ずしも予期されない
    ジャンプするコードは、例外終了の可能性を念頭に置いて記述されている場合とされていない場合があります。もともとそのように書かれている場合、それを念頭に置いて維持されていない可能性があります。考えてみてください:メモリリーク、ファイル記述子リーク、ソケットリーク、誰が知っていますか?
  4. メンテナンスの複雑さ
    例外の処理を飛び回るコードを維持するのは困難です。
40
DigitalRoss

例外のスローは、ある程度、gotoステートメントに似ています。フロー制御のためにそれを行うと、理解できないスパゲッティコードで終わります。さらに悪いことに、ジャンプが正確にどこに行くのかさえわからない場合もあります(つまり、特定のコンテキストで例外をキャッチしていない場合)。これは、保守性を高める「驚き最小の原則」に露骨に違反しています。

22

例外があると、プログラムの状態について推論するのが難しくなります。たとえば、C++では、関数が例外的に安全であることを確認するために、必要がない場合よりも特別な検討を行う必要があります。

その理由は、例外なく、関数呼び出しが戻るか、プログラムを最初に終了できるためです。例外を除いて、関数呼び出しは、プログラムを返すか、プログラムを終了するか、どこかのキャッチブロックにジャンプすることができます。そのため、目の前のコードを見るだけでは、制御の流れをたどることはできなくなります。呼び出された関数がスローできるかどうかを知る必要があります。コントロールがどこに行くかを気にするか、それとも現在のスコープを離れることだけを気にするかに応じて、何を投げることができ、どこでキャッチされるかを知る必要があるかもしれません。

このため、「本当に例外的な状況でない限り、例外を使用しないでください」と言われます。 「本当に例外的」とは、「エラーの戻り値で処理することのメリットがコストを上回っている状況が発生した」ことを意味します。そうです、これは空のステートメントのようなものですが、「本当に例外的」という本能があれば、それは大まかな目安になります。人々がフロー制御について話すとき、彼らは(キャッチブロックを参照せずに)ローカルで推論する能力が戻り値の利点であることを意味します。

Javaには、C++よりも「本当に例外的」という広い定義があります。 C++プログラマーは、Javaプログラマーよりも、関数の戻り値を見たいと思う可能性が高いので、Java「本当に例外的」とは「できるこの関数の結果としてnull以外のオブジェクトを返さない」。C++では、「呼び出し元が続行できるかどうかは非常に疑わしい」という意味になる可能性が高くなります。したがって、Java stream throws ifファイルを読み取ることはできませんが、C++ストリーム(デフォルト)はエラーを示す値を返します。ただし、すべての場合において、呼び出し元に強制的に書き込む必要のあるコードの問題です。確かにコーディングスタイルの問題です。コードがどのように見えるか、そしてどのくらいの「例外安全」推論を実行したいかに対して、どのくらいの「エラーチェック」コードを記述したいかについて合意に達する必要があります。

すべての言語にわたる幅広いコンセンサスは、エラーがどの程度回復可能であるかという観点からこれを行うのが最善であるようです(回復不可能なエラーは例外のあるコードを生成しないためですが、それでも自分自身をチェックして返す必要があります-エラーリターンを使用するコードのエラー)。したがって、人々は「私が呼び出すこの関数は例外をスローする」という意味で「[〜#〜] i [〜#〜]は続行できない」と期待するようになります「it続行できません」だけではありません。これは例外に固有のものではなく、単なる習慣ですが、他の優れたプログラミング手法と同様に、他の方法で試しても結果を享受しなかった賢い人々によって提唱された習慣です。私も、あまりにも多くの例外を投げるという悪い経験をしました。ですから、個人的には、状況について何かが例外を特に魅力的にしない限り、「本当に例外的」という観点から考えています。

ところで、コードの状態について推論する以外に、パフォーマンスへの影響もあります。現在、パフォーマンスを気にする資格のある言語では、例外は通常安価です。それらは、「ああ、結果はエラーです。それなら、私もエラーで終了するのが最善です」という複数のレベルよりも高速になる可能性があります。古き良き時代には、例外をスローし、それをキャッチし、次のことを続けると、あなたがしていることが役に立たなくなるほど遅くなるのではないかという本当の恐れがありました。したがって、その場合、「本当に例外的」とは、「状況が非常に悪いため、恐ろしいパフォーマンスはもはや問題にならない」という意味です。これはもはや当てはまりません(ただし、タイトループの例外は依然として目立ちます)。うまくいけば、「本当に例外的な」の定義を柔軟にする必要がある理由を示しています。

16
Steve Jessop

本当にコンセンサスはありません。例外をスローすることの「適切さ」は、言語自体の標準ライブラリ内の既存のプラクティスによって示唆されることが多いため、問題全体はやや主観的です。 C++標準ライブラリは、言うよりもはるかに少ない頻度で例外をスローします。Java標準ライブラリは、無効なユーザー入力などの予想されるエラーに対しても、ほとんど常に例外を優先します(例:Scanner.nextInt)。これは、いつ例外をスローするのが適切かについての開発者の意見に大きく影響すると思います。

C++プログラマーとして、私は個人的に非常に「例外的な」状況のために例外を予約することを好みます。メモリ不足、ディスクスペース不足、黙示録が発生したなど。しかし、これが絶対に正しい方法であるとは主張しません。

11
Charles Salvia

例外はめったに使用されるべきではないと思います。だが。

すべてのチームとプロジェクトが例外を使用する準備ができているわけではありません。例外を使用するには、プログラマーの高度な資格、特別な技術、および大きなレガシーの例外安全でないコードの欠如が必要です。巨大な古いコードベースがある場合、ほとんどの場合、例外安全ではありません。書き直したくないと思います。

例外を広範囲に使用する場合は、次のようにします。

  • 例外安全性とは何かについて人々に教える準備をしてください
  • 生のメモリ管理を使用しないでください
  • rAIIを幅広く使用する

一方、強力なチームを持つ新しいプロジェクトで例外を使用すると、コードがよりクリーンになり、保守が容易になり、さらに高速になる可能性があります。

  • エラーを見逃したり無視したりすることはありません
  • 低レベルで間違ったコードをどう処理するかを実際に知らずに、リターンコードのチェックを書く必要はありません。
  • 例外安全なコードを書くことを余儀なくされると、それはより構造化されます
7

2009年1月20日編集

私はちょうど読んでいました マネージコードのパフォーマンスの改善に関するこのMSDNの記事 そしてこの部分は私にこの質問を思い出させました:

例外をスローすることによるパフォーマンスコストは重要です。構造化された例外処理はエラー状態を処理するための推奨される方法ですが、エラー状態が発生する例外的な状況でのみ例外を使用するようにしてください。通常の制御フローに例外を使用しないでください。

もちろん、これは.NET専用であり、特に高性能アプリケーションを開発している人(私のような)を対象としています。ですから、それは明らかに普遍的な真実ではありません。それでも、私たち.NET開発者はたくさんいるので、注目に値するものだと感じました。

[〜#〜]編集[〜#〜]

OK、まず第一に、1つのことをまっすぐにしましょう:私はパフォーマンスの質問について誰かとの戦いを選ぶつもりはありません。一般的に、実際、私は時期尚早の最適化が罪であると信じている人々に同意する傾向があります。ただし、2つの点を指摘しておきます。

  1. ポスターは、例外は控えめに使用されるべきであるという一般通念の背後にある客観的な論理的根拠を求めています。読みやすさと適切なデザインについて、私たちが望むすべてについて話し合うことができます。しかし、これらはどちらの側でも議論する準備ができている人々の主観的な問題です。ポスターはこれを知っていると思います。事実、例外を使用してプログラムフローを制御することは、多くの場合、非効率的な方法です。いいえ、alwaysではなく、しばしば。これが、赤身の肉を食べたりワインを控えめに飲んだりするのと同じように、例外を控えめに使用することが合理的なアドバイスである理由です。

  2. 正当な理由のない最適化と効率的なコードの記述には違いがあります。これに対する当然の結果として、最適化されていなくても堅牢なものを書くことと、単に非効率的なものを書くことには違いがあります。例外処理のようなことについて議論するとき、彼らは根本的に異なることを話し合っているので、実際にはお互いを超えて話しているだけだと思う​​ことがあります。

私のポイントを説明するために、次のC#コード例を検討してください。

例1:無効なユーザー入力の検出

これは、私が例外と呼ぶものの例ですabuse

_int value = -1;
string input = GetInput();
bool inputChecksOut = false;

while (!inputChecksOut) {
    try {
        value = int.Parse(input);
        inputChecksOut = true;

    } catch (FormatException) {
        input = GetInput();
    }
}
_

このコードは、私にはばかげています。もちろんworks。誰もそれについて議論していません。しかし、shouldは次のようになります。

_int value = -1;
string input = GetInput();

while (!int.TryParse(input, out value)) {
    input = GetInput();
}
_

例2:ファイルの存在を確認する

このシナリオは実際には非常に一般的だと思います。それは確かにseemsファイルI/Oを扱うので、多くの人にとってはるかに「受け入れられる」でしょう。

_string text = null;
string path = GetInput();
bool inputChecksOut = false;

while (!inputChecksOut) {
    try {
        using (FileStream fs = new FileStream(path, FileMode.Open)) {
            using (StreamReader sr = new StreamReader(fs)) {
                text = sr.ReadToEnd();
            }
        }

        inputChecksOut = true;

    } catch (FileNotFoundException) {
        path = GetInput();
    }
}
_

これは十分に合理的なようですよね?ファイルを開こうとしています。そこにない場合は、その例外をキャッチして別のファイルを開こうとします...何が問題なのですか?

本当に何もありません。ただし、しない例外をスローするこの代替案を検討してください。

_string text = null;
string path = GetInput();

while (!File.Exists(path)) path = GetInput();

using (FileStream fs = new FileStream(path, FileMode.Open)) {
    using (StreamReader sr = new StreamReader(fs)) {
        text = sr.ReadToEnd();
    }
}
_

もちろん、これら2つのアプローチのパフォーマンスが実際に同じである場合、これは実際には純粋に教義上の問題になります。それでは、見てみましょう。最初のコード例では、10000個のランダムな文字列のリストを作成しましたが、いずれも適切な整数を表していないため、最後に有効な整数文字列を追加しました。上記の両方のアプローチを使用して、これらは私の結果でした:

try/catchブロックの使用:25.455 seconds
_int.TryParse_の使用:1.637 ミリ秒

2番目の例では、基本的に同じことを行いました。10000個のランダムな文字列のリストを作成しましたが、いずれも有効なパスではなく、最後に有効なパスを追加しました。結果は次のとおりです。

try/catchブロックの使用:29.989 seconds
_File.Exists_の使用:22.820 ミリ秒

多くの人がこれに「ええ、そうですね、10,000の例外をスローしてキャッチすることは非常に非現実的です。これは、結果を誇張します」と答えます。もちろんそうです。 1つの例外をスローすることと、自分で不正な入力を処理することの違いは、ユーザーには気付かれません。例外の使用は、これら2つの場合、1,000倍から10,000倍以上遅い同じように読みやすい代替アプローチよりも遅いという事実は変わりません。

そのため、以下にGetNine()メソッドの例を含めました。耐えられないほど遅いまたは容認できないほど遅いということではありません。それはそれよりも遅いということですshouldbe ...理由もなく

繰り返しますが、これらは2つの例にすぎません。 courseの場合、例外を使用した場合のパフォーマンスへの影響はそれほど深刻ではない場合があります(Pavelの権利、結局のところ、実装によって異なります)。私が言っているのは、事実に直面しましょう。上記のような場合、例外をスローしてキャッチすることはGetNine()に類似しています。それは何かをするための非効率的な方法ですそれは簡単にもっとうまくやれるでしょう


あなたは、これが理由を知らずに誰もが時流に乗ったような状況の1つであるかのように論理的根拠を求めています。しかし実際には答えは明白であり、あなたはすでにそれを知っていると思います。 例外処理のパフォーマンスはひどいです。

OK、特にビジネスシナリオには問題ないかもしれませんが、比較的言えば、例外をスロー/キャッチすると、多くの場合、必要以上のオーバーヘッドが発生します。あなたはそれを知っています、私はそれを知っています:ほとんどの場合、プログラムの流れを制御するために例外を使用している場合、あなたはただスローコードを書いているだけです。

あなたは尋ねたほうがいいかもしれません:なぜこのコードは悪いのですか?

_private int GetNine() {
    for (int i = 0; i < 10; i++) {
        if (i == 9) return i;
    }
}
_

この関数のプロファイルを作成すると、通常のビジネスアプリケーションで非常に高速に実行されることがわかると思います。それは、それがはるかに良くできることを達成するためのひどく非効率的な方法であるという事実を変えません。

それが、例外の「虐待」について話すときの人々の意味です。

7
Dan Tao

例外に関する経験則はすべて、主観的な用語に帰着します。いつ使用するか、いつ使用しないかについて、厳密かつ迅速に定義することを期待するべきではありません。 「例外的な状況でのみ」。良い循環定義:例外は例外的な状況のためのものです。

例外を使用する場合は、「このコードが1つのクラスか2つのクラスかをどのように知ることができますか?」と同じバケットに分類されます。これは、一部はスタイルの問題であり、一部は好みです。例外はツールです。それらは使用および悪用される可能性があり、2つの間の境界線を見つけることはプログラミングの芸術とスキルの一部です。

多くの意見があり、トレードオフが必要です。あなたに話しかける何かを見つけて、それに従ってください。

6
Ned Batchelder

例外がめったに使用されるべきではないというわけではありません。例外的な状況でのみスローする必要があるというだけです。たとえば、ユーザーが間違ったパスワードを入力した場合、それは例外ではありません。

理由は単純です。例外が関数を突然終了し、スタックをcatchブロックに伝播します。このプロセスは非常に計算コストが高くなります。C++は「通常の」関数呼び出しのオーバーヘッドがほとんどないように例外システムを構築するため、例外が発生すると、どこに行くかを見つけるために多くの作業を行う必要があります。さらに、コードのすべての行で例外が発生する可能性があるためです。例外を頻繁に発生させる関数fがある場合は、tryを呼び出すたびにcatch/fブロックを使用するように注意する必要があります。これは、インターフェイスと実装の結合がかなり悪いです。

6
rlbond

エラー処理に対する私のアプローチは、3つの基本的なタイプのエラーがあるということです。

  • エラーサイトで処理できる奇妙な状況。これは、ユーザーがコマンドラインプロンプトで無効な入力を入力した場合に発生する可能性があります。正しい動作は、単にユーザーに文句を言って、この場合はループすることです。別の状況は、ゼロ除算である可能性があります。これらの状況は実際にはエラー状況ではなく、通常は入力の誤りが原因です。
  • 前のような状況ですが、エラーサイトでは処理できません。たとえば、ファイル名を取得してその名前のファイルを解析する関数がある場合、ファイルを開くことができない可能性があります。この場合、エラーに対処できません。これは例外が光るときです。 Cアプローチを使用する(フラグとして無効な値を返し、問題を示すためにグローバルエラー変数を設定する)のではなく、コードは代わりに例外をスローできます。呼び出し元のコードは、例外を処理できるようになります。たとえば、ユーザーに別のファイル名を要求する場合などです。
  • 起こってはならない状況。これは、クラス不変条件に違反した場合、または関数が無効なパラメーターなどを受け取った場合です。これは、コード内の論理障害を示しています。失敗のレベルに応じて、例外が適切な場合もあれば、(assertのように)即時終了を強制することが望ましい場合もあります。一般に、これらの状況は、コードのどこかで何かが壊れていることを示しており、他のものが正しいとは事実上信頼できません。メモリの破損が横行している可能性があります。あなたの船は沈んでいます、降りてください。

言い換えると、例外は、対処できる問題があるが、気付いた場所では対処できない場合です。対処できない問題は、単にプログラムを強制終了する必要があります。すぐに対処できる問題は、単に対処する必要があります。

5
coppro

この問題については C++の例外に関する記事 で説明しました。

関連する部分:

ほとんどの場合、例外を使用して「通常の」フローに影響を与えることはお勧めできません。セクション3.1ですでに説明したように、例外は非表示のコードパスを生成します。これらのコードパスは、エラー処理シナリオでのみ実行される場合、ほぼ間違いなく受け入れられます。ただし、他の目的で例外を使用する場合、「通常の」コード実行は表示部分と非表示部分に分割され、コードの読み取り、理解、拡張が非常に困難になります。

5

私はここでいくつかの答えを読みました。私はまだこのすべての混乱が何であるかについて驚いています。私はこのすべての例外== spagettyコードに強く同意しません。混乱して、C++の例外処理を好まない人がいるということです。 C++の例外処理についてどのように学んだかはわかりませんが、数分以内にその影響を理解しました。これは1996年頃で、私はOS/2用のborlandC++コンパイラを使用していました。いつ例外を使用するかを決めるのに問題はありませんでした。私は通常、フォールブルな元に戻すアクションをC++クラスにラップします。このような元に戻すアクションには、次のものがあります。

  • システムハンドルの作成/破棄(ファイル、メモリマップ、WIN32 GUIハンドル、ソケットなど)
  • ハンドラーの設定/設定解除
  • メモリの割り当て/割り当て解除
  • ミューテックスの要求/解放
  • 参照カウントのインクリメント/デクリメント
  • ウィンドウの表示/非表示

機能的なラッパーがあるより。システムコール(前者のカテゴリに分類されない)をC++にラップすること。例えば。ファイルからの読み取り/ファイルへの書き込み。何かが失敗した場合、エラーに関する完全な情報を含む例外がスローされます。

次に、障害にさらに情報を追加するための例外のキャッチ/再スローがあります。

全体的なC++例外処理は、よりクリーンなコードにつながります。コードの量が大幅に削減されます。最後に、コンストラクターを使用してフォールブルリソースを割り当て、そのような障害が発生した後も破損のない環境を維持できます。

このようなクラスを複雑なクラスにチェーンすることができます。あるメンバー/ベースオブジェクトのコンストラクターが実行されると、同じオブジェクト(以前に実行された)の他のすべてのコンストラクターが正常に実行されたことを信頼できます。

5
user678269

例外は、従来の構造(ループ、if、関数など)と比較して、非常に珍しいフロー制御方法です。通常の制御フロー構造(ループ、if、関数呼び出しなど)は、すべての通常の状況を処理できます。日常的に発生する例外に到達した場合は、コードの構造を検討する必要があります。

ただし、通常の構成では簡単に処理できない特定のタイプのエラーがあります。壊滅的な障害(リソース割り当ての失敗など)は低レベルで検出できますが、おそらくそこでは処理できないため、単純なifステートメントでは不十分です。これらのタイプの障害は、通常、はるかに高いレベルで処理する必要があります(たとえば、ファイルの保存、エラーのログ記録、終了)。従来の方法(戻り値など)を使用してこのようなエラーを報告しようとすると、面倒でエラーが発生しやすくなります。さらに、この奇妙で異常な障害を処理するために、中間レベルのAPIのレイヤーにオーバーヘッドを注入します。オーバーヘッドにより、これらのAPIのクライアントの注意がそらされ、制御できない問題について心配する必要があります。例外は、問題の検出とそのハンドラーの間のすべてのレイヤーからはほとんど見えない大きなエラーに対して非ローカル処理を行う方法を提供します。

クライアントが文字列を使用してParseIntを呼び出し、文字列に整数が含まれていない場合、直接の呼び出し元はおそらくエラーを気にし、それをどうするかを知っています。したがって、そのようなものの失敗コードを返すようにParseIntを設計します。

一方、メモリがひどく断片化されているためにバッファを割り当てることができなかったためにParseIntが失敗した場合、呼び出し元はそれについて何をすべきかを知ることができません。この異常なエラーを、これらの基本的な障害を処理するいくつかのレイヤーまでバブルする必要があります。それはその間のすべての人に負担をかけます(彼らは彼ら自身のAPIでエラー受け渡しメカニズムに対応しなければならないからです)。例外により、これらのレイヤーをスキップすることができます(必要なクリーンアップが確実に行われるようにします)。

低レベルのコードを記述している場合、従来のメソッドをいつ使用するか、いつ例外をスローするかを決めるのは難しい場合があります。低レベルのコードが決定を下す必要があります(スローするかどうか)。しかし、何が期待され、何が例外的であるかを本当に知っているのは、より高いレベルのコードです。

3
Adrian McCarthy

C++にはいくつかの理由があります。

まず、例外がどこから来ているのかを確認するのは難しいことがよくあります(ほとんどすべてのものからスローされる可能性があるため)。したがって、catchブロックはCOMEFROMステートメントのようなものです。 GO TOでは、どこから来ているのか(ランダムな関数呼び出しではなく、ステートメント)とどこに行くのか(ラベル)がわかっているため、GOTOよりも悪いです。これらは基本的に、Cのsetjmp()およびlongjmp()の潜在的にリソースセーフなバージョンであり、誰もそれらを使用したくありません。

次に、C++にはガベージコレクションが組み込まれていないため、リソースを所有するC++クラスはデストラクタでガベージコレクションを削除します。したがって、C++例外処理では、システムはスコープ内のすべてのデストラクタを実行する必要があります。 GCがあり、Javaのように実際のコンストラクターがない言語では、例外をスローすることははるかに負担が少なくなります。

第三に、Bjarne Stroustrup、標準化委員会、およびさまざまなコンパイラの作成者を含むC++コミュニティは、例外は例外的であるべきだと想定してきました。一般的に、言語文化に反対する価値はありません。実装は、例外がまれであるという仮定に基づいています。より良い本は例外を例外として扱います。優れたソースコードは、いくつかの例外を使用します。優れたC++開発者は、例外を例外として扱います。それに逆らうには、正当な理由が必要です。私が見るすべての理由は、それらを例外的に保つ側にあります。

3
David Thornley

これは、制御フローとして例外を使用する悪い例です。

int getTotalIncome(int incomeType) {
   int totalIncome= 0;
   try {
      totalIncome= calculateIncomeAsTypeA();
   } catch (IncorrectIncomeTypeException& e) {
      totalIncome= calculateIncomeAsTypeB();
   }

   return totalIncome;
}

これは非常に悪いですが、あなたは書くべきです:

int getTotalIncome(int incomeType) {
   int totalIncome= 0;
   if (incomeType == A) {
      totalIncome= calculateIncomeAsTypeA();
   } else if (incomeType == B) {
      totalIncome= calculateIncomeAsTypeB();
   }
   return totalIncome;
}

この2番目の例では、明らかに(デザインパターン戦略の使用など)リファクタリングが必要ですが、例外が制御フローを対象としていないことをよく示しています。

例外にはいくつかのパフォーマンスペナルティも関連付けられていますが、パフォーマンスの問題は次のルールに従う必要があります:「時期尚早の最適化はすべての悪の根源です」

非常に実用的な理由の1つは、プログラムをデバッグするときに、アプリケーションをデバッグするためにFirst Chance Exceptions(Debug-> Exceptions)をオンにすることがよくあることです。多くの例外が発生している場合、どこが「間違っている」のかを見つけるのは非常に困難です。

また、悪名高い「キャッチスロー」のようないくつかのアンチパターンにつながり、実際の問題を難読化します。詳細については、 ブログ投稿 この件について作成しました。

2
Nate Zaugg
  1. 保守性:上記の人々が述べたように、帽子をかぶったときに例外をスローすることは、gotoを使用することに似ています。
  2. 相互運用性:例外を使用している場合、C++ライブラリをC/Pythonモジュールとインターフェースすることはできません(少なくとも簡単ではありません)。
  3. パフォーマンスの低下:RTTIは、追加のオーバーヘッドを課す例外のタイプを実際に見つけるために使用されます。したがって、例外は、一般的に発生するユースケース(ユーザーが文字列ではなくintを入力した場合など)の処理には適していません。
2
Sridhar Iyer

例外はできるだけ使用しないほうがいいです。例外により、開発者は実際のエラーである場合とそうでない場合がある条件を処理する必要があります。問題の例外が致命的な問題であるか、すぐに処理されなければならない問題であるかどうかの定義。

それに対する反論は、怠惰な人々が自分の足で自分自身を撃つためにもっとタイプする必要があるということです。

Googleのコーディングポリシーでは、特にC++では 例外を使用しないでください としています。アプリケーションは、例外を処理する準備ができていないか、準備ができています。そうでない場合、例外はおそらくアプリケーションが終了するまでそれを伝播します。

スロー例外を使用したライブラリを見つけるのは決して楽しいことではなく、それらを処理する準備ができていませんでした。

2
s1n

例外は、安全な方法で現在のコンテキストから(最も単純な意味では現在のスタックフレームから外れますが、それだけではありません)あなたを解放するメカニズムであると言えます。これは、構造化プログラミングが後藤に最も近いものです。意図された方法で例外を使用するには、現在行っていることを続行できず、現在の時点でそれを処理できない状況が必要です。したがって、たとえば、ユーザーのパスワードが間違っている場合は、falseを返すことで続行できます。ただし、UIサブシステムがユーザーにプロンプ​​トを表示することすらできないと報告した場合、単に「ログインに失敗しました」と返すのは誤りです。現在のレベルのコードは、単に何をすべきかを知りません。そのため、例外メカニズムを使用して、何をすべきかを知っている可能性のある上記の誰かに責任を委任します。

2
Arkadiy

例外をスローする正当なケース:

  • ファイルを開こうとしましたが、そこにありません。FileNotFoundExceptionがスローされます。

違法なケース:

  • ファイルが存在しない場合にのみ何かを実行したい場合は、ファイルを開こうとしてから、catchブロックにコードを追加します。

アプリケーションのフローを特定のポイントまで分割するにしたいときに例外を使用します。このポイントは、その例外のcatch(...)が存在する場所です。たとえば、大量のプロジェクトを処理する必要があることは非常に一般的であり、各プロジェクトは他のプロジェクトとは独立して処理する必要があります。したがって、プロジェクトを処理するループにはtry ... catchブロックがあり、プロジェクトの処理中に何らかの例外がスローされると、そのプロジェクトのすべてがロールバックされ、エラーがログに記録され、次のプロジェクトが処理されます。人生は続く。

存在しないファイルや無効な式などには例外を使うべきだと思います。 範囲テスト/データ型テスト/ファイルの存在/他に簡単で安価な代替手段がある場合は、例外を使用しないでください。 この種のロジックはコードを理解しにくくするため、範囲テスト/データ型テスト/ファイルの存在/それに代わる簡単で安価な代替手段がある場合は、例外を使用しないでください。

RecordIterator<MyObject> ri = createRecordIterator();
try {
   MyObject myobject = ri.next();
} catch(NoSuchElement exception) {
   // Object doesn't exist, will create it
}

これはより良いでしょう:

RecordIterator<MyObject> ri = createRecordIterator();
if (ri.hasNext()) {
   // It exists! 
   MyObject myobject = ri.next();
} else {
   // Object doesn't exist, will create it
}

回答にコメントを追加:

たぶん私の例はあまり良くありませんでした-2番目の例ではri.next()が例外をスローするべきではありません。もしそうなら、本当に例外的な何かがあり、他の場所で他のアクションを実行する必要があります。例1を頻繁に使用すると、開発者は特定の例外ではなく一般的な例外をキャッチし、例外が予期したエラーによるものであると想定しますが、他の原因による可能性もあります。結局、例外はアプリケーションフローの一部になり、例外ではないため、実際の例外は無視されます。

これに関するコメントは、私の答え自体以上のものを追加するかもしれません。

1
Ravi Wallau

例外の目的は、ソフトウェアのフォールトトレランスを実現することです。ただし、関数によってスローされたすべての例外に応答する必要があると、抑制につながります。例外は、プログラマーにルーチンで特定の問題が発生する可能性があること、およびクライアントプログラマーがこれらの条件を認識し、必要に応じて対応する必要があることを認めさせる正式な構造にすぎません。

正直なところ、例外はプログラミング言語に追加された恨みであり、エラーケースの処理の責任を直接の開発者から将来の開発者に移す正式な要件を開発者に提供します。

優れたプログラミング言語は、C++とJavaで例外を知っているので、例外をサポートしていないと思います。関数からのあらゆる種類の戻り値に代替フローを提供できるプログラミング言語を選択する必要があります。プログラマーは、ルーチンのすべての形式の出力を予測し、自分のやり方があれば、それらを別のコードファイルで処理する責任があります。

0
Excalibur2000

基本的に、例外は構造化されておらず、フロー制御の形式を理解するのが困難です。これは、通常のプログラムフローの一部ではないエラー状態を処理するときに必要です。これは、エラー処理ロジックがコードの通常のフロー制御を乱雑にしないようにするためです。

IMHO例外は、呼び出し元がエラー処理コードの記述を怠った場合、またはエラーが直接の呼び出し元よりもコールスタックの上位で処理される可能性がある場合に、適切なデフォルトを提供する場合に使用する必要があります。正しいデフォルトは、妥当な診断エラーメッセージを表示してプログラムを終了することです。非常識な代替策は、プログラムが誤った状態で足を引きずり、クラッシュするか、後で静かに悪い出力を生成し、ポイントを診断するのが難しくなることです。 「エラー」がプログラムフローの通常の部分であり、呼び出し元がそれをチェックすることを合理的に忘れることができない場合は、例外を使用しないでください。

0
dsimcha

私の2セント:

エラーが発生しないかのようにプログラムできるので、例外を使用するのが好きです。したがって、私のコードは読みやすく、あらゆる種類のエラー処理が散在しているわけではありません。もちろん、エラー処理(例外処理)は最後(catchブロック)に移動されるか、呼び出しレベルの責任と見なされます。

私にとっての良い例は、ファイル処理またはデータベース処理のいずれかです。すべてが正常であると想定し、最後に、または何らかの例外が発生した場合にファイルを閉じます。または、例外が発生したときにトランザクションをロールバックします。

例外の問題は、すぐに非常に冗長になることです。コードを非常に読みやすくし、通常の流れに焦点を当てることを目的としていましたが、一貫して使用する場合、ほとんどすべての関数呼び出しをtry/catchブロックでラップする必要があり、目的を達成できなくなります。

前述のParseIntの場合、例外のアイデアが好きです。値を返すだけです。パラメータが解析可能でない場合は、例外をスローします。それは一方であなたのコードをよりきれいにします。呼び出しレベルでは、次のようなことを行う必要があります

try 
{
   b = ParseInt(some_read_string);
} 
catch (ParseIntException &e)
{
   // use some default value instead
   b = 0;
}

コードはクリーンです。このようにParseIntが散らばっている場合は、例外を処理してデフォルト値を返すラッパー関数を作成します。例えば。

int ParseIntWithDefault(String stringToConvert, int default_value=0)
{
   int result = default_value;
   try
   {
     result = ParseInt(stringToConvert);
   }
   catch (ParseIntException &e) {}

   return result;
}

結論として、私が議論を通して見逃したのは、エラー条件をもっと無視できるので、例外によってコードがより簡単に読みやすくなるという事実でした。問題:

  • 例外はまだどこかで処理する必要があります。追加の問題:c ++には、関数がスローする可能性のある例外を指定できる構文がありません(Javaなど))。したがって、呼び出しレベルは、処理する必要のある例外を認識しません。
  • すべての関数をtry/catchブロックでラップする必要がある場合、コードが非常に冗長になることがあります。しかし、これでも意味がある場合もあります。

そのため、バランスをとることが難しい場合があります。

0
nathanvda

「めったに使わない」というのは正しい文章ではないと思います。私は「例外的な状況でのみ投げる」ことを好みます。

多くの人が、なぜ例外を通常の状況で使用すべきではないのかを説明しています。例外には、エラー処理と純粋にエラー処理の権利があります。

私は他の点に焦点を当てます:

もう1つは、パフォーマンスの問題です。コンパイラは、それらを高速化するのに長い間苦労しました。現在の正確な状態はわかりませんが、制御フローに例外を使用すると、他の問題が発生します。プログラムが遅くなります。

その理由は、例外は非常に強力なgotoステートメントであるだけでなく、離れるすべてのフレームのスタックを巻き戻す必要があるためです。したがって、暗黙的にスタック上のオブジェクトも分解する必要があります。したがって、それを意識せずに、例外を1回スローするだけで、実際には多くのメカニズムが関与することになります。プロセッサは非常に多くのことをしなければなりません。

そのため、知らないうちにプロセッサをエレガントに焼き付けることになります。

したがって、例外は例外的な場合にのみ使用してください-意味:実際のエラーが発生したとき!

0
Juergen

次の場合に例外を使用します。

  • ローカルから回復できないエラーが発生しましたAND
  • エラーがプログラムから回復されない場合は終了する必要があります。

エラーを回復できる場合(ユーザーが数字の代わりに「Apple」と入力した場合)、回復します(もう一度入力を求める、デフォルト値に変更するなど)。

ローカルからエラーを回復できないが、アプリケーションは続行できる場合(ユーザーがファイルを開こうとしたが、ファイルが存在しない場合)、エラーコードが適切です。

ローカルからエラーを回復できず、回復せずにアプリケーションを続行できない場合(メモリ/ディスク容量などが不足している場合)、例外が正しい方法です。

0
Bill

保守的に使用すべきだと誰が言ったのですか?フロー制御に例外を使用しないでください。それだけです。そして、例外がすでにスローされている場合、誰が例外のコストを気にしますか?

0
user215054