280

ほとんどの人は、デストラクタから例外をスローしないと言っています。そうすると、未定義の動作が発生します。Stroustrup は、「ベクトル デストラクタはすべての要素に対して明示的にデストラクタを呼び出すことを強調しています。これは、要素デストラクタがスローした場合、ベクトルの破壊が失敗することを意味します...デストラクタからスローされた例外から保護する良い方法は実際にはありません。そのため、ライブラリは要素デストラクタがスローするかどうかは保証しません」(付録 E3.2 より)

この記事では、デストラクタのスローは多かれ少なかれ問題ないと言っているようです。

だから私の質問はこれです - デストラクタからスローすると未定義の動作になる場合、デストラクタ中に発生したエラーをどのように処理しますか?

クリーンアップ操作中にエラーが発生した場合、それを無視しますか? スタックで処理できる可能性があるが、デストラクタで正しく処理できないエラーである場合、デストラクタから例外をスローするのは理にかなっていませんか?

明らかに、この種のエラーはまれですが、発生する可能性があります。

4

17 に答える 17

212

デストラクタから例外をスローするのは危険です。
別の例外がすでに伝播している場合、アプリケーションは終了します。

#include <iostream>

class Bad
{
    public:
        // Added the noexcept(false) so the code keeps its original meaning.
        // Post C++11 destructors are by default `noexcept(true)` and
        // this will (by default) call terminate if an exception is
        // escapes the destructor.
        //
        // But this example is designed to show that terminate is called
        // if two exceptions are propagating at the same time.
        ~Bad() noexcept(false)
        {
            throw 1;
        }
};
class Bad2
{
    public:
        ~Bad2()
        {
            throw 1;
        }
};


int main(int argc, char* argv[])
{
    try
    {
        Bad   bad;
    }
    catch(...)
    {
        std::cout << "Print This\n";
    }

    try
    {
        if (argc > 3)
        {
            Bad   bad; // This destructor will throw an exception that escapes (see above)
            throw 2;   // But having two exceptions propagating at the
                       // same time causes terminate to be called.
        }
        else
        {
            Bad2  bad; // The exception in this destructor will
                       // cause terminate to be called.
        }
    }
    catch(...)
    {
        std::cout << "Never print this\n";
    }

}

これは基本的に次のように要約されます。

危険なこと (つまり、例外をスローする可能性があるもの) は、パブリック メソッドを介して (必ずしも直接ではなく) 実行する必要があります。クラスのユーザーは、パブリック メソッドを使用して潜在的な例外をキャッチすることで、これらの状況を処理できる可能性があります。

次に、デストラクタはこれらのメソッドを呼び出してオブジェクトを終了します (ユーザーが明示的に呼び出していない場合) が、スローされた例外はすべてキャッチされて破棄されます (問題の修正を試みた後)。

したがって、事実上、責任をユーザーに渡します。ユーザーが例外を修正できる立場にある場合、適切な関数を手動で呼び出してエラーを処理します。オブジェクトのユーザーが (オブジェクトが破棄されるため) 心配していない場合は、デストラクタが処理を任せられます。

例:

std::fstream

close() メソッドは、例外をスローする可能性があります。ファイルが開かれている場合、デストラクタは close() を呼び出しますが、デストラクタから例外が伝播されないようにします。

そのため、ファイル オブジェクトのユーザーがファイルを閉じることに関連する問題に対して特別な処理を行いたい場合は、手動で close() を呼び出して例外を処理します。一方、彼らが気にしない場合は、デストラクタが状況を処理するために残されます。

Scott Myers は、彼の著書「Effective C++」で、このテーマに関する優れた記事を書いています。

編集:

どうやら「より効果的なC ++」にもあるようです。
項目11:例外がデストラクタから離れないようにする

于 2008-09-24T21:35:59.893 に答える
63

このデストラクタは「スタックの巻き戻し」の一部として呼び出される可能性があるため、デストラクタからスローするとクラッシュする可能性があります。スタックの巻き戻しは、例外がスローされたときに行われる手順です。この手順では、"try" から例外がスローされるまでにスタックにプッシュされたすべてのオブジェクトが終了され、それらのデストラクタが呼び出されます。また、この手順の間、一度に 2 つの例外を処理することはできないため、別の例外のスローは許可されません。したがって、これにより abort() の呼び出しが引き起こされ、プログラムがクラッシュし、制御が OS に戻ります。

于 2008-12-17T15:22:03.820 に答える
52

特定のケースに対する一般的なアドバイスにやみくもに従うのではなく、ここで区別する必要があります。

以下では、オブジェクトのコンテナーの問題と、コンテナー内のオブジェクトの複数の d'tors に直面して何をすべきかを無視していることに注意してください。(一部のオブジェクトはコンテナに入れるのに適していないため、部分的に無視することもできます。)

クラスを 2 つのタイプに分割すると、問題全体を考えるのが簡単になります。クラスドクターには、次の 2 つの異なる責任があります。

  • (R) セマンティクスの解放 (別名、そのメモリの解放)
  • (C)セマンティクスのコミット(別名、ディスクへのファイルのフラッシュ)

質問をこのように見ると、(R) セマンティクスが dtor からの例外を引き起こすべきではないと主張できると思います。エラーチェックも提供します。void free(void* p);

データを正常にフラッシュする必要があるファイル オブジェクトや、dtor でコミットを行う (「スコープ ガード」) データベース接続のような (C) セマンティクスを持つオブジェクトは、別の種類のものです。アプリケーション レベル) であり、何も起こらなかったかのように続行するべきではありません。

RAII ルートに従い、d'tors に (C) セマンティクスを持つオブジェクトを許可する場合、そのような d'tors がスローできる奇妙なケースも許可する必要があると思います。したがって、そのようなオブジェクトをコンテナーに入れてはならないということになりますterminate()。また、別の例外がアクティブなときに commit-dtor がスローされた場合でも、プログラムは停止できるということになります。


エラー処理 (コミット/ロールバック セマンティクス) と例外に関しては、Andrei Alexandrescuによる良い話があります: C++ でのエラー処理 / 宣言型制御フロー( NDC 2014で開催)

詳細については、Folly ライブラリがツールUncaughtExceptionCounter用に を実装する方法を説明しています。ScopeGuard

(他の人も同様の考えを持っていたことに注意する必要があります。)

話は d'tor からのスローに焦点を当てていませんが、d'tor からのスローのタイミングに関する問題を取り除くために今日使用できるツールを示しています。

将来的には、これに対する std 機能が存在する可能性があります。N3614およびそれに関する議論を参照してください。

Upd '17: このための C++17 std 機能はstd::uncaught_exceptionsafaikt です。cppref の記事を簡単に引用します。

ノート

int-returningが使用される例uncaught_exceptionsは ... ... 最初にガード オブジェクトを作成し、そのコンストラクターでキャッチされなかった例外の数を記録します。出力は、foo() がスローしない限り、ガード オブジェクトのデストラクタによって実行されます (この場合、デストラクタで捕捉されなかった例外の数は、コンストラクタが観察した数よりも多くなります) 。

于 2010-11-04T16:07:37.493 に答える
20

デストラクタからのスローについて自問する本当の質問は、「呼び出し元はこれで何ができるか?」ということです。デストラクタからスローすることによって作成された危険を相殺する、例外を使用して実際にできる便利なことはありますか?

Fooオブジェクトを破棄し、Fooデストラクタが例外を投げた場合、合理的に何ができるでしょうか? ログに記録することも、無視することもできます。それで全部です。Fooオブジェクトが既になくなっているため、「修正」できません。最良の場合、例外をログに記録し、何も起こらなかったかのように続行します (またはプログラムを終了します)。デストラクタからスローして未定義の動作を引き起こす可能性があることは本当に価値がありますか?

于 2008-09-24T21:42:05.417 に答える
13

C++ の ISO ドラフト (ISO/IEC JTC 1/SC 22 N 4411) から

したがって、デストラクタは通常、例外をキャッチし、デストラクタの外に伝播させないようにする必要があります。

3 try ブロックから throw 式までのパス上に構築された自動オブジェクトのデストラクタを呼び出すプロセスは、「スタックの巻き戻し」と呼ばれます。[注: スタックの巻き戻し中に呼び出されたデストラクタが例外で終了した場合、std::terminate が呼び出されます (15.5.1)。したがって、デストラクタは通常、例外をキャッチし、デストラクタの外に伝播させないようにする必要があります。— エンドノート]

于 2009-04-11T03:00:42.170 に答える
12

危険ですが、読みやすさ/コードの理解のしやすさの観点からも意味がありません。

この状況であなたが尋ねなければならないのは

int foo()
{
   Object o;
   // As foo exits, o's destructor is called
}

例外をキャッチする必要があるのは何ですか? foo の呼び出し元は? それとも foo がそれを処理する必要がありますか?foo の呼び出し元が foo 内部のオブジェクトを気にする必要があるのはなぜですか? 言語がこれを意味を成すように定義する方法はあるかもしれませんが、それは判読できず、理解するのが難しいでしょう。

さらに重要なことは、オブジェクトのメモリはどこに行くのでしょうか? オブジェクトが所有していたメモリはどこに行きますか? まだ割り当てられていますか (表向きはデストラクタが失敗したため)? オブジェクトがスタック空間にあったことも考慮してください。

次に、このケースを検討してください

class Object
{ 
   Object2 obj2;
   Object3* obj3;
   virtual ~Object()
   {
       // What should happen when this fails? How would I actually destroy this?
       delete obj3;

       // obj 2 fails to destruct when it goes out of scope, now what!?!?
       // should the exception propogate? 
   } 
};

obj3 の削除が失敗した場合、失敗しないことが保証されている方法で実際に削除するにはどうすればよいですか? その私の記憶くそー!

ここで、最初のコード スニペットで、Object3 がヒープ上にある間はスタック上にあるため、Object が自動的に消えることを考慮してください。Object3 へのポインターがなくなったので、あなたは一種の SOL です。メモリ リークがあります。

物事を行うための1つの安全な方法は次のとおりです

class Socket
{
    virtual ~Socket()
    {
      try 
      {
           Close();
      }
      catch (...) 
      {
          // Why did close fail? make sure it *really* does close here
      }
    } 

};

こちらのFAQもご覧ください

于 2008-09-24T21:48:07.740 に答える
7

私は、デストラクタでスローする「スコープ ガード」パターンが多くの状況で、特に単体テストで役立つと考えるグループに属しています。ただし、C++11 では、std::terminateデストラクタに暗黙的にnoexcept.

Andrzej Krzemieński は、スローするデストラクタのトピックに関する素晴らしい投稿をしています。

noexcept彼は、C++11 にはデストラクタのデフォルトをオーバーライドするメカニズムがあると指摘しています。

C++11 では、デストラクタは暗黙的に として指定されnoexceptます。仕様を追加せず、次のようにデストラクタを定義したとしても:

  class MyType {
        public: ~MyType() { throw Exception(); }            // ...
  };

コンパイラはnoexcept、デストラクタに仕様を目に見えないように追加します。std::terminateこれは、二重例外の状況がなくても、デストラクタが例外をスローした瞬間に呼び出されることを意味します。デストラクタがスローできるようにすることに本当に決めている場合は、これを明示的に指定する必要があります。次の 3 つのオプションがあります。

  • noexcept(false)デストラクタを,として明示的に指定します。
  • すでにデストラクタを として指定している別のクラスからクラスを継承しますnoexcept(false)
  • すでにデストラクタを として指定している非静的データ メンバをクラスに配置しますnoexcept(false)

最後に、デストラクタをスローすることにした場合は、二重例外 (例外のためにスタックが巻き戻されている間にスローする) のリスクを常に認識しておく必要があります。これにより が呼び出されますがstd::terminate、それが必要なことはめったにありません。この動作を回避するには、を使用して新しい例外をスローする前に、既に例外があるかどうかを簡単に確認できますstd::uncaught_exception()

于 2017-01-02T16:02:04.373 に答える
6

デストラクタは、他のデストラクタのチェーン内で実行されている可能性があります。直接の呼び出し元によってキャッチされない例外をスローすると、複数のオブジェクトが一貫性のない状態のままになる可能性があるため、クリーンアップ操作でエラーを無視するよりもさらに多くの問題が発生する可能性があります。

于 2008-09-24T21:41:01.173 に答える
5

他の誰もが、デストラクタのスローがひどい理由を説明しています...それについて何ができますか? 失敗する可能性のある操作を実行している場合は、クリーンアップを実行し、任意の例外をスローできる別のパブリック メソッドを作成します。ほとんどの場合、ユーザーはそれを無視します。ユーザーがクリーンアップの成功/失敗を監視したい場合は、単純に明示的なクリーンアップ ルーチンを呼び出すことができます。

例えば:

class TempFile {
public:
    TempFile(); // throws if the file couldn't be created
    ~TempFile() throw(); // does nothing if close() was already called; never throws
    void close(); // throws if the file couldn't be deleted (e.g. file is open by another process)
    // the rest of the class omitted...
};
于 2009-01-19T05:39:54.597 に答える
3

優れた、包括的で正確な主な回答への追加として、あなたが参照している記事についてコメントしたいと思います-「デストラクタで例外をスローすることはそれほど悪くない」と書かれています。

この記事では、「例外をスローする代替手段は何ですか」という行を取り、各代替手段に関するいくつかの問題をリストしています。そうすることで、問題のない代替手段が見つからないため、例外をスローし続ける必要があると結論付けます。

問題は、代替手段でリストされている問題のどれもが、「プログラムの未定義の動作」である例外動作ほど悪いものではないことです。著者の反対意見には、「審美的に醜い」や「スタイルの悪さを助長する」などがあります。さて、あなたはどちらがいいですか?スタイルの悪いプログラム、または未定義の動作を示したプログラムですか?

于 2009-01-20T19:57:08.563 に答える
2

Q: 私の質問はこれです。デストラクタからスローすると未定義の動作が発生する場合、デストラクタ中に発生したエラーをどのように処理しますか?

A: いくつかのオプションがあります。

  1. 他の場所で何が起こっているかに関係なく、例外がデストラクタから流出するようにします。その際、 std::terminate が続く可能性があることに注意してください (または恐れることさえあります)。

  2. デストラクタから例外を流さないでください。可能であれば、ログに書き込むことができます。大きな赤い悪いテキストがいくつかあります。

  3. my fave : std::uncaught_exceptionfalse が返された場合、例外を発生させます。true が返された場合は、ロギング アプローチにフォールバックします。

しかし、d'tors を投入するのはよいことでしょうか。

私は上記のほとんどに同意します。それが可能な場合、スローはデストラクタで回避するのが最善です。しかし、それが起こり得ることを受け入れて、うまく対処するのが最善の場合もあります。上記の3つを選びます。

デストラクタからスローするのが実際に素晴らしいアイデアである奇妙なケースがいくつかあります。「要チェック」エラーコードのように。関数から返される値型です。呼び出し元が含まれているエラー コードを読み取り/チェックすると、返された値は黙って破棄されます。 ただし、戻り値が範囲外になるまでに返されたエラー コードが読み取られていない場合、そのデストラクタから何らかの例外がスローされます。

于 2010-03-18T14:57:57.800 に答える
2

だから私の質問はこれです-デストラクタからスローすると未定義の動作が発生する場合、デストラクタ中に発生するエラーをどのように処理しますか?

主な問題は次のとおりです。失敗することはできません。結局、失敗に失敗するとはどういう意味ですか?データベースへのトランザクションのコミットが失敗し、失敗しない (ロールバックできない) 場合、データの整合性はどうなりますか?

デストラクタは通常のパスと例外的な (失敗した) パスの両方に対して呼び出されるため、それら自体が失敗することはありません。

これは概念的に難しい問題ですが、多くの場合、解決策は失敗が失敗しないようにする方法を見つけることです。たとえば、データベースは、外部データ構造またはファイルにコミットする前に変更を書き込む場合があります。トランザクションが失敗した場合、ファイル/データ構造は破棄できます。その場合、確実に行う必要があるのは、その外部構造/ファイルからの変更を、失敗することのないアトミック トランザクションにコミットすることだけです。

実用的な解決策は、おそらく失敗時に失敗する可能性が天文学的にありそうもないことを確認することです。

私にとって最も適切な解決策は、クリーンアップ ロジックが失敗しないように非クリーンアップ ロジックを記述することです。たとえば、既存のデータ構造をクリーンアップするために新しいデータ構造を作成したい場合は、その補助構造を事前に作成して、デストラクタ内で作成する必要がないようにすることができます。

確かに、これは言うは易く行うは難しですが、私が考える唯一の適切な方法です。通常の実行パスと例外的な実行パスの別々のデストラクタ ロジックを記述する機能が必要だと思うことがあります。これは、デストラクタが両方を処理しようとすることで、2 倍の責任を負っているように感じることがあるためです (例としては、明示的な却下を必要とするスコープ ガードがあります)。 ; 例外的な破壊パスと非例外的な破壊パスを区別できる場合、これは必要ありません)。

それでも、究極の問題は、失敗することはできないということであり、すべてのケースで完全に解決するのは難しい概念設計の問題です。多数の小さなオブジェクトが相互に作用する複雑な制御構造に巻き込まれすぎず、代わりに少しかさばる方法で設計をモデル化すると、簡単になります (例: 粒子全体を破壊するデストラクタを備えた粒子システム)。システムであり、パーティクルごとに個別の非自明なデストラクタではありません)。この種のより粗いレベルで設計をモデル化すると、処理する重要なデストラクタが少なくなり、多くの場合、デストラクタが失敗しないようにするために必要なメモリ/処理のオーバーヘッドを許容できます。

そして、これは当然、デストラクタの使用頻度を減らす最も簡単な解決策の 1 つです。上記のパーティクルの例では、おそらくパーティクルを破棄/削除する際に、何らかの理由で失敗する可能性があるいくつかのことを行う必要があります。その場合、例外的なパスで実行される可能性のあるパーティクルの dtor を介してそのようなロジックを呼び出す代わりに、パーティクル システムがパーティクルを削除するときにすべてをパーティクル システムで実行することができます。パーティクルの削除は、例外的でないパスで常に実行される場合があります。システムが破壊された場合、おそらくすべてのパーティクルをパージすることができ、失敗する可能性のある個々のパーティクル除去ロジックに煩わされることはありませんが、失敗する可能性のあるロジックは、パーティクル システムの通常の実行中に 1 つまたは複数のパーティクルを除去するときにのみ実行されます。

自明ではないデストラクタを使用して多くの小さなオブジェクトを処理することを避けると、そのような解決策がしばしば発生します。例外安全であることがほとんど不可能に思える混乱に巻き込まれる可能性があるのは、すべてが重要な dtor を持つ多くの小さなオブジェクトに巻き込まれる場合です。

それを指定するもの (基本クラスの noexcept 仕様を継承する必要がある仮想関数を含む) がスローされる可能性のあるものを呼び出そうとした場合に、nothrow/noexcept が実際にコンパイラ エラーに変換されると、非常に役立ちます。このようにして、スローされる可能性のあるデストラクタを実際に誤って記述した場合、コンパイル時にこれらすべてのものをキャッチできます。

于 2018-01-05T13:50:02.300 に答える
0

オブジェクトの作成が成功したことを示すために例外をスローすることが有効な方法であるコンストラクターとは異なり、デストラクタでは例外をスローすべきではありません。

この問題は、スタックの巻き戻しプロセス中にデストラクタから例外がスローされると発生します。その場合、コンパイラは、スタックの巻き戻しプロセスを続行するか、新しい例外を処理するかを判断できない状況に置かれます。最終結果は、プログラムがすぐに終了することです。

したがって、最善の方法は、デストラクタで例外をまったく使用しないことです。代わりに、メッセージをログ ファイルに書き込みます。

于 2013-10-27T03:31:54.980 に答える
0

アラームイベントを設定します。通常、アラーム イベントは、オブジェクトのクリーンアップ中に障害を通知するより適切な形式です

于 2013-02-25T07:42:25.230 に答える
-1

私は現在、クラスがデストラクタから積極的に例外をスローするべきではなく、代わりに失敗する可能性のある操作を実行するためのパブリック「クローズ」メソッドを提供するべきであるというポリシー (非常に多くの人が言っている) に従っています...

...しかし、ベクトルのようなコンテナ型クラスのデストラクタは、それらに含まれるクラスからスローされた例外をマスクすべきではないと私は信じています。この場合、実際には、自分自身を再帰的に呼び出す「free/close」メソッドを使用しています。はい、再帰的に言いました。この狂気には方法があります。例外の伝播はスタックの存在に依存しています。単一の例外が発生した場合、残りの両方のデストラクタが引き続き実行され、ルーチンが戻ると保留中の例外が伝播されます。これは素晴らしいことです。複数の例外が発生した場合、(コンパイラによっては) 最初の例外が伝播するか、プログラムが終了しますが、どちらでもかまいません。再帰がスタックをオーバーフローするほど多くの例外が発生した場合、何か重大な問題があり、誰かがそれを発見することになりますが、これも問題ありません。個人的には、

ポイントは、コンテナがニュートラルのままであり、デストラクタから例外をスローすることに関して、コンテナが動作するか、または誤動作するかを決定するのは、含まれるクラス次第であるということです。

于 2010-02-02T06:21:18.730 に答える