9

私は、少なくとも最良のシナリオ以上のプログラムを作成しようとする駆け出しのプログラマーです。私は Herb Sutter の「Exceptional C++」を読んでおり、これまでに例外安全性の章を 3 回読みました。ただし、彼が提示した例 (スタック) を除いて、例外の安全性と速度を正確に比較する必要がある時期と、そうするのがばかげている時期はよくわかりません。

たとえば、私の現在の宿題プロジェクトは二重リンク リストです。すでにこれらのいくつかをプログラムしたので、時間をかけて ES などのより深い概念に取り掛かりたいと思いました。

これが私のポップフロント機能です:

void List::pop_front()
{
    if(!head_)
        throw std::length_error("Pop front:  List is empty.\n");
    else
    {
        ListElem *temp = head_;
        head_          = head_->next;
        head_->prev    = 0;
        delete temp;
        --size_;
    }
}

これにはいくつかのジレンマがありました。

1) リストが失敗した場合、本当にエラーをスローする必要がありますか? リストのユーザーに try {] catch() {} ステートメント (これも遅い) を強制的に実行させるのではなく、単純に何もせずに戻るべきではないでしょうか。

2) 複数のエラー クラスがあります (さらに、クラスに実装することを先生が要求する ListException もあります)。カスタムエラークラスは本当に必要ですか?特定の例外クラスをいつ使用するかについての一般的なガイドはありますか? (たとえば、範囲、長さ、境界はすべて同じように聞こえます)

3) 例外をスローしたすべてのコードが完了するまで、プログラムの状態を変更してはならないことを知っています。これが、size_last をデクリメントしている理由です。この単純な例では、これは本当に必要ですか? delete がスローできないことはわかっています。0 に割り当てたときに head_->prev がスローされることはありますか? (頭は最初のノードです)

私の push_back 関数:

void List::push_back(const T& data)
{
    if(!tail_)
    {
        tail_ = new ListElem(data, 0, 0);
        head_ = tail_;
    }
    else
    {
    tail_->next = new ListElem(data, 0, tail_);
    tail_ = tail_->next;
    }
    ++size_;
}

1) C++ プログラムでは何でも失敗する可能性があるとよく耳にします。ListElem のコンストラクターが失敗するかどうか (または ing 中に tail_ が失敗newするかどうか) をテストするのは現実的ですか?

2) 型が構造に対して実行可能であることを確認するために、データの型をテストする必要がありますか (現在、typedef int Tすべてをテンプレート化するまでは単純です)。

これらは非常に単純な例であることは理解していますが、実際に優れた ES を実践する必要がある場合とそうでない場合について、現在混乱しています。

4

5 に答える 5

9

リストが失敗した場合、本当にエラーをスローする必要がありますか? リストのユーザーに try {] catch() {} ステートメント (これも遅い) を強制的に実行させるのではなく、単純に何もせずに戻るべきではないでしょうか。

絶対に例外をスローします。

ユーザーは、リストが空の場合に何が起こったのかを知っている必要があります。そうしないと、デバッグするのが大変になります。ユーザーは、try/catch ステートメントの使用を強制されません。例外が予期しないものである場合 (つまり、プログラマーのエラーが原因でのみ発生する可能性がある場合)、それをキャッチしようとする理由はありません。例外がキャッチされない場合、std::terminate にフォールスルーされます。これは非常に便利な動作です。とにかく、try/catch ステートメント自体も遅くはありません。例外の実際のスローとスタックの巻き戻しにはどのようなコストがかかりますか。例外がスローされない場合、コストはほとんどかかりません。

複数のエラー クラスがあります (さらに、クラスに実装することを先生が要求する ListException もあります)。カスタムエラークラスは本当に必要ですか?特定の例外クラスをいつ使用するかについての一般的なガイドはありますか? (たとえば、範囲、長さ、境界はすべて同じように聞こえます)

できるだけ具体的にしてください。これを行うには、独自のエラー クラスを使用するのが最善の方法です。継承を使用して、関連する例外をグループ化します (呼び出し元が例外をより簡単にキャッチできるようにします)。

例外をスローしたすべてのコードが完了するまで、プログラムの状態を変更すべきではないことはわかっています。これが、size_last をデクリメントしている理由です。この単純な例では、これは本当に必要ですか? delete がスローできないことはわかっています。0 に割り当てたときに head_->prev がスローされることはありますか? (頭は最初のノードです)

が null の場合、( への代入試行の一部として)head_それを逆参照すると、未定義の動作になります。例外をスローすることは、未定義の動作の結果である可能性がありますが、可能性は低いです (そのようなことはばかげていると見なされる言語では、コンパイラがあなたの手を握るために邪魔にならないようにする必要があります;))、そして1つではありません未定義の動作は未定義の動作であるため、私たちが心配しているのは、とにかくプログラムがすでに間違っていることを意味し、間違っている方法をより正しくしようとしても意味がありません。head_->prev

head_さらに、それが null ではないことをすでに明示的にチェックしています。したがって、スレッドで何もしていないと仮定すると、問題はありません。

C++ プログラムでは何でも失敗する可能性があるとよく耳にします。

それは少し妄想的です。:)

ListElem のコンストラクターが失敗するかどうか (または newing 中の tail_) をテストするのは現実的ですか?

が失敗した場合new、 のインスタンスstd::bad_allocがスローされます。例外をスローすることはまさにここで発生させたいことであるため、何もしたくない、またはする必要はありません。ただ伝播させてください。エラーをある種のリストの例外として再記述しても、実際には有用な情報が追加されず、物事がさらに不明瞭になる可能性があります。

コンストラクター ListElem が失敗した場合、それは例外をスローして失敗するはずです。999 対 1 の確率で、それも失敗させる必要があります。

ここで重要なのは、ここで例外がスローされるたびに、クリーンアップ作業を行う必要がないということです。これは、まだリストを変更しておらず、構築/新規作成されたオブジェクトが Officially Never Existed(TM) であるためです。そのコンストラクターが例外に対して安全であることを確認してください。呼び出しがメモリの割り当てに失敗した場合new、コンストラクターは呼び出されません。

心配する必要があるのは、同じ場所に複数の割り当てを行う場合です。この場合、2 番目の割り当てが失敗した場合は、(それが何であれ) 例外をキャッチし、最初の割り当てをクリーンアップして、再スローするようにする必要があります。そうしないと、最初の割り当てがリークします。

型が構造に対して実行可能であることを確認するために、データの型 (現在、すべてをテンプレート化するまで単純な typedef int T) をテストする必要があるでしょうか?

型はコンパイル時にチェックされます。実行時にそれらについて現実的に何もすることはできませんし、現実的にする必要もありません。(すべての型チェックが必要ない場合は、なぜ型名だけをすべての場所に入力することを強制する言語を使用しているのですか? :))

于 2010-12-19T12:12:35.107 に答える
8

例外の安全性と速度を両立させるために、正確にいつ努力すべきかよくわかりません

常に例外の安全性に努める必要があります。「例外の安全性」とは、「問題が発生した場合に例外をスローする」という意味ではないことに注意してください。これは、「weak、strong、nothrow の 3 つの例外保証のいずれかを提供する」ことを意味します。例外のスローはオプションです。例外の安全性は、エラーが発生したときにコードが正しく動作できることをコードの呼び出し元が満足できるようにするために必要です。

例外に関して、さまざまな C++ プログラマー/チームからの非常に異なるスタイルが表示されます。それらを頻繁に使用する人もいれば、ほとんど使用しない人もいます (または、厳密にはまったく使用しない人もいますが、今ではかなりまれだと思います。Google はおそらく最も (悪) 有名な例です。興味がある場合は、C++ スタイル ガイドでその理由を確認してください)。組み込みデバイスとゲームの内部は、C++ で完全に例外を回避している人々の例を見つける可能性が最も高い場所です)。標準の iostreams ライブラリを使用すると、I/O エラーが発生したときにストリームが例外をスローするかどうかについて、ストリームにフラグを設定できます。デフォルトでは to ではありません。これは、例外が存在する他のほとんどすべての言語のプログラマーにとって驚きです。

リストが失敗した場合、本当にエラーをスローする必要がありますか?

「リスト」が失敗するのではなく、失敗するのpop_frontはリストが空のときに呼び出されることです。クラスのすべての操作を一般化することはできません。失敗時に常に例外をスローする必要があるため、特定のケースを考慮する必要があります。この場合、少なくとも 5 つの妥当なオプションがあります。

  • 何かがポップされたかどうかを示す値を返します。発信者はこれで好きなことをするか、無視することができます。
  • pop_frontリストが空のときに呼び出すのは未定義の動作であることを文書化し、コード内の の可能性を無視しますpop_front。空の標準コンテナーをポップするのは UB であり、一部の標準ライブラリの実装には、特にリリース ビルドではチェック コードが含まれていません。
  • 未定義の動作であることを文書化しますが、とにかくチェックを行い、プログラムを中止するか、例外をスローします。おそらく、デバッグ ビルドでのみチェックを行うことができます (これassertが目的です)。その場合、デバッガー ブレークポイントをトリガーするオプションもあるかもしれません。
  • リストが空の場合、呼び出しは効果がないことを文書化します。
  • リストが空の場合に例外がスローされることを文書化します。

最後のものを除くこれらすべては、関数が「nothrow」保証を提供できることを意味します。どちらを選択するかは、API をどのように見せたいか、および呼び出し元がバグを見つけるのにどのような支援を提供したいかによって異なります。例外をスローしても、直接の呼び出し元に例外をキャッチさせるわけではないことに注意してください。例外は、エラーから回復できるコード (またはオプションでプログラムの最上部) によってのみキャッチされます。

個人的には、ユーザー エラーに対して例外をスローしない傾向があり、空のリストをポップすることはユーザー エラーであると言う傾向もあります。これは、デバッグ モードですべての種類のチェックを実行することが有用ではないという意味ではなく、通常、そのようなチェックがすべてのモードで実行されることを保証する API を定義しないということです。

そのようなことのために本当にカスタムエラークラスが必要ですか

いいえ、これは回避可能なエラーであるため、必要ありません。呼び出し元は、 を呼び出す前にリストが空でないことを確認することで、常にスローされないようにすることができますpop_frontstd::logic_errorスローするのに完全に合理的な例外です。特別な例外クラスを使用する主な理由は、呼び出し元がその例外だけをキャッチできるようにするためです。呼び出し元が特定のケースでそれを行う必要があると考えるかどうかはあなた次第です。

0 に割り当てたときに head_->prev がスローされることはありますか?

プログラムが何らかの形で未定義の動作を引き起こした場合を除きます。そうです、その前にサイズdeleteを減らすことができ、ListElem のデストラクタがスローできないことが確実であれば、前にサイズを減らすことができます。また、デストラクタを記述するときは、それがスローされないようにする必要があります。

C++ プログラムでは何でも失敗する可能性があるとよく耳にします。ListElem のコンストラクターが失敗するかどうか (または newing 中の tail_) をテストするのは現実的ですか?

すべてが失敗する可能性があるというのは真実ではありません。理想的には、関数は提供する例外保証を文書化する必要があります。これにより、関数がスローできるかどうかがわかります。それらが本当に十分に文書化されている場合は、スローできるすべてのものと、どのような状況でスローするかをリストします。

失敗したかどうかをテストするべきではありませんnew。 からの例外がnewあれば、関数から呼び出し元に伝播できるようにする必要があります。次に、メモリ不足を示すためにpush_frontスローできることを文書化できstd::bad_allocます。また、のコピーコンストラクターによってスローされるものは何でもスローできることも可能ですT(の場合は何もありませんint)。これを関数ごとに個別に文書化する必要はない場合があります。複数の関数をカバーする一般的なメモで十分な場合があります。呼び出された関数push_frontがスローできる場合、それがスローできるものの1つがbad_alloc. また、含まれている要素が例外をスローした場合、それらの例外が伝播される可能性があることは、テンプレート コンテナーのユーザーにとって驚くべきことではありません。

型が構造に対して実行可能であることを確認するために、データの型 (現在、すべてをテンプレート化するまで単純な typedef int T) をテストする必要があるでしょうか?

おそらく、T に必要なすべてがコピー構築可能で代入可能であるように構造を書くことができます。このために特別なテストを追加する必要はありません。誰かがテンプレートに対して実行する操作をサポートしない型でテンプレートをインスタンス化しようとすると、コンパイル エラーが発生します。ただし、要件を文書化する必要があります。

于 2010-12-19T12:32:26.967 に答える
4

それは長い質問です。番号が振られたすべての質問を受けます1)

1) リストが失敗した場合、本当にエラーをスローする必要がありますか? リストのユーザーに try {] catch() {} ステートメント (これも遅い) を強制的に実行させるのではなく、単純に何もせずに戻るべきではないでしょうか。

いいえ。ユーザーがパフォーマンスを気にする場合、ポップして例外をキャッチするのではなく、ポップを試みる前に長さをチェックします。例外は、最初に長さをチェックするのを忘れた場合にユーザーに通知することです。何もしないと、後で現れる微妙な問題が発生する可能性があり、デバッグがより困難になります。

1) C++ プログラムでは何でも失敗する可能性があるとよく耳にします。ListElem のコンストラクターが失敗するかどうか (または newing 中の tail_) をテストするのは現実的ですか?

たとえば、コンストラクターは、メモリが不足すると失敗する可能性がありますが、この場合、null を返すのではなく、例外をスローする必要があります。したがって、コンストラクターの失敗を明示的にテストする必要はありません。詳細については、この質問を参照してください。

于 2010-12-19T11:56:36.977 に答える
2

C++ プログラムでは何でも失敗する可能性があるとよく耳にします。ListElem のコンストラクターが失敗するかどうか (または newing 中の tail_) をテストするのは現実的ですか?

はい、現実的です。そうしないと、プログラムがメモリ不足になり、割り当てが失敗した場合 (またはコンストラクターが他の内部的な理由で失敗した場合)、後で問題が発生します。

基本的に、コードがその API で宣言されていることを完全に実行できない場合は、いつでも失敗を通知する必要があります。

唯一の違いは、戻り値または例外を介して失敗を通知する方法です。パフォーマンスの考慮事項が存在する場合、戻り値は例外よりも優れている場合があります。ただし、どちらのアプローチでも、呼び出し元で特別なエラー キャッチ コードが必要です。

于 2010-12-19T12:02:32.220 に答える
1

最初の一連の質問:

  1. はい、@Markの回答のすべての理由から、スローする必要があります。(彼に+1)
  2. これは必ずしも必要ではありませんが、発信者の負担を大幅に軽減できます。例外処理の利点の 1 つは、コードをローカライズして、特定のクラスのエラーを 1 か所でまとめて処理できることです。特定の例外タイプをスローすることにより、呼び出し元がそのエラーを明確にキャッチできるようにするか、スローした特定の例外のスーパークラスをキャッチして、より一般的にすることができます。
  3. あなたのすべてのステートメントはelse、nothrow 保証を提供します。

2 番目のセットの場合:

  1. いいえ、テストするのは現実的ではありません。基礎となるコンストラクターが何をスローできるかはわかりません。それは期待されるアイテム (つまりstd::bad_alloc) かもしれませんし、何か奇妙なもの (つまりint) かもしれません。したがって、それを処理できる唯一の方法は、catch(...)それを悪である :) の中に入れることです:)

    一方、既存のメソッドは、ifブロック内に作成されたダミーのエンド ノードがリンク リストのデストラクタによって無効にされる限り、既に例外に対して安全です。(つまり、news の後のすべてが nothrow を提供します)

  2. Tデストラクタを除いて、すべての操作がスローできると仮定してください。

于 2010-12-19T12:12:10.657 に答える