web-dev-qa-db-ja.com

デストラクタから例外をスローする

ほとんどの人は、neverはデストラクタから例外をスローすると言います-そうすると、未定義の動作になります。 Stroustrupは、」ベクトルデストラクタがすべての要素に対して明示的にデストラクタを呼び出すことを指摘します。これは、要素デストラクタがスローすると、ベクトル破壊が失敗することを意味します。デストラクタからスローされた例外に対して、ライブラリは要素デストラクタがスローしても保証しません」(付録E3.2から)

この記事 別の言い方をするようです-スローするデストラクタは多かれ少なかれ大丈夫です。

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

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

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

240
Greg Rogers

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

#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()を手動で呼び出して例外を処理します。一方、彼らが気にしない場合、デストラクタは状況を処理するために残されます。

スコットマイヤーズの著書「Effective C++」には、このテーマに関する優れた記事があります。

編集:

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

183
Martin York

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

52
Gal Goldman

ここでgeneralのアドバイスをspecificに盲目的に従うのではなく、ここでdifferentiateする必要がありますケース。

次のは、オブジェクトのコンテナーの問題と、コンテナー内のオブジェクトの複数のd'torに直面した場合の処理​​を無視することに注意してください。 (また、一部のオブジェクトはコンテナに入れるのに適していないため、部分的に無視することができます。)

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

  • (R)セマンティクスを解放する(別名メモリを解放する)
  • (C)commitセマンティクス(別名flushfile to disk)

このように質問を見ると、(R)セマンティクスはdtorから例外を決して引き起こすべきではないと主張できると思いますa)それについて何もできない、b)多くのフリーリソース操作はしないエラーチェックも提供します。例えばvoidfree(void* p);

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

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


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

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

others も同様のアイデアを持っていたことに注意する必要があります。)

トークはd'torからの投げに焦点を当てていませんが、todayを使用して 投げるときの問題を取り除くことができるツールを示しています d'torから。

の中に 未来、 そこ 五月 このための標準機能である、 N3614 を参照 それについての議論

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

ノート

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

46
Martin Ba

デストラクタからのスローについて自問する本当の質問は、「呼び出し側はこれで何ができるのか?」です。デストラクタから投げることによって生じる危険を相殺する、例外でできることは実際にありますか?

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

19
Derek Park

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

したがって、デストラクタは一般に例外をキャッチし、デストラクタから伝播しないようにする必要があります。

3 tryブロックからthrow式へのパス上に構築された自動オブジェクトのデストラクタを呼び出すプロセスは、「スタックアンワインド」と呼ばれます。 (15.5.1)。したがって、デストラクタは一般に例外をキャッチし、デストラクタから伝播しないようにする必要があります。 —終了ノート]

12
lothar

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

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

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

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

さらに重要なことは、オブジェクトのメモリはどこに行くのでしょうか?オブジェクトが所有するメモリはどこに行きますか?それはまだ割り当てられていますか?オブジェクトがstack spaceにあったことも考慮してください。

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

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

12
Doug T.

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

7
Franci Penov

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

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

問題は、それがリストに挙げている問題が、例外の振る舞いほど悪いことではないということです。これは、「プログラムの未定義の振る舞い」です。著者の異議には、「審美的にい」および「悪いスタイルを奨励する」などがあります。さて、どちらを選びますか?スタイルの悪いプログラム、または未定義の動作を示したプログラムですか?

5
DJClayworth

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

例えば:

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...
};
5
Tom

私は、デストラクタにスローする「スコープ付きガード」パターンが多くの状況で、特に単体テストで役立つと考えるグループに所属しています。ただし、C++ 11では、デストラクタに暗黙的にnoexceptアノテーションが付けられているため、デストラクタをスローするとstd::terminateが呼び出されることに注意してください。

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

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

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

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

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

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

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

4
GaspardP

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

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

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

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

  3. my favestd::uncaught_exceptionがfalseを返す場合、例外が流出します。 trueが返された場合、ロギングアプローチにフォールバックします。

しかし、d'torsを投入するのは良いことですか?

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

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

2
MartinP

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

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

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

1
Matthew

Martin Ba(上記)は正しい軌道に乗っています。RELEASEロジックとCOMMITロジックのアーキテクチャは異なります。

リリースの場合:

エラーを食べる必要があります。メモリを解放したり、接続を閉じたりします。システム内の他の誰もこれらのものを再び見るべきではなく、リソースをOSに返しています。ここで実際のエラー処理が必要と思われる場合は、おそらくオブジェクトモデルの設計上の欠陥の結果です。

コミットの場合:

これは、std :: lock_guardのようなものがミューテックスに提供しているのと同じ種類のRAIIラッパーオブジェクトが必要な場所です。これらでは、コミットロジックをdtor AT ALLに入れません。専用のAPIがあり、RAIIがそれをdtorsでコミットし、そこでエラーを処理するラッパーオブジェクト。デストラクタで例外をうまくキャッチできることを忘れないでください。それらを発行することは致命的です。これにより、異なるラッパー(std :: unique_lockとstd :: lock_guardなど)を構築するだけでポリシーとさまざまなエラー処理を実装でき、コミットロジックの呼び出しを忘れないようにします。そもそもそれをdtorに入れる正当な理由。

1
user3726672

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

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

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

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

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

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

確かに、これは実際に行うよりもはるかに簡単に言うことができますが、それが私がそれについて進むために見る唯一の本当に適切な方法です。時々、デストラクタは両方を処理しようとすることで2倍の責任があるように感じるため、通常の実行パス用に別個のデストラクタロジックを記述する必要があると思います(例は明示的な解雇を必要とするスコープガードです) ;例外的な破壊経路と非例外的な破壊経路を区別できれば、これは必要ありません。

それでも究極の問題は、失敗することはできないということであり、すべての場合に完全に解決するのは難しい概念設計の問題です。多数の小さなオブジェクトが相互作用する複雑な制御構造にあまり包まれず、代わりにややかさばった方法でデザインをモデル化すると簡単になります(例:粒子全体を破壊するデストラクタを持つ粒子システム粒子ごとの独立した非自明なデストラクタではありません)。このような粗いレベルで設計をモデル化すると、対処する非自明なデストラクタが少なくなり、デストラクタが失敗しないことを確認するために必要なメモリ/処理のオーバーヘッドを支払う余裕もあります。

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

多くの場合、非自明なデストラクタを使用して多数の小さなオブジェクトを扱うことを避けた場合に生じるようなソリューションがあります。あなたが例外安全であることがほとんど不可能であると思われる混乱に巻き込まれることができるのは、すべてが重要なdtorsを持っているたくさんの小さなオブジェクトに巻き込まれるときです。

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

0
Dragon Energy

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

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

そのため、デストラクタで例外を完全に使用しないことをお勧めします。代わりに、ログファイルにメッセージを書き込みます。

0
Devesh Agrawal

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

0
MRN