壊滅的な例外

デストラクタで例外をスローするのが悪い理由についてもう一度



多くのC ++の専門家(例: Herb Sutter )は、デストラクタで例外をスローすることは悪いことを教えてくれます。なぜなら、既にスローされた例外でスタックのプロモーション中にデストラクタに入ることができ、その瞬間に別の例外がスローされた場合、結果はstdと呼ばれます::終了() 。 このトピックに関するC ++ 17言語標準(以降、私は自由に利用できるドラフトN4713のバージョンを参照します)から次のことがわかります。







18.5.1 std :: terminate()関数[except.terminate]



1状況によっては、より微妙なエラー処理手法のために例外処理を放棄する必要があります。 [注:



これらの状況は次のとおりです。



...



(1.4)スタックのアンワインド(18.2)中にオブジェクトの破棄が例外をスローして終了した場合、または



...



-終了ノート]

簡単な例を見てみましょう。







#include <iostream> class PrintInDestructor { public: ~PrintInDestructor() noexcept { std::cerr << "~PrintInDestructor() invoked\n"; } }; void throw_int_func() { std::cerr << "throw_int_func() invoked\n"; throw 1; } class ThrowInDestructor { public: ~ThrowInDestructor() noexcept(false) { std::cerr << "~ThrowInDestructor() invoked\n"; throw_int_func(); } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowInDestructor bad; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* c) { std::cerr << "Catched const char* exception: " << c << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; }
      
      





結果:







 ~ThrowInDestructor() invoked throw_int_func() invoked ~PrintInDestructor() invoked terminate called after throwing an instance of 'int' Aborted
      
      





PrintInDestructorデストラクタはまだ呼び出されていることに注意してください。 2番目の例外をスローした後、スタックプロモーションは中断されません。 この主題に関する規格(同じ段落18.5.1)は次のように述べています。







2 ...一致するハンドラが見つからない状況では、

std :: terminate()が呼び出される前にスタックがアンワインドされるかどうかは実装定義です。 で

ハンドラー(18.3)の検索で、関数の最も外側のブロックに遭遇する状況

非スロー例外仕様(18.4)、スタックがアンワインドされるかどうかは実装定義

std :: terminate()が呼び出される前に、部分的に巻き戻されるか、まったく巻き戻されません...

この例は、スタックの昇格が続くすべての場所で、 GCC (8.2、7.3)およびClang6.0、5.0 )のいくつかのバージョンでテストしました。 実装定義が異なるコンパイラに出会った場合は、コメントに書いてください。







また、デストラクタから例外がスローされたときにのみスタックが巻き戻されると、 std :: terminate()が呼び出されることにも注意してください。 例外をキャッチし、それ以上スローしないtry / catchブロックがデストラクタ内にある場合、これは外部例外のスタックの昇格を中断しません。







 class ThrowCatchInDestructor { public: ~ThrowCatchInDestructor() noexcept(false) { try { throw_int_func(); } catch (int i) { std::cerr << "Catched int in ~ThrowCatchInDestructor(): " << i << "\n"; } } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowCatchInDestructor good; std::cerr << "ThrowCatchInDestructor instance created\n"; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; }
      
      





ディスプレイ







 ThrowCatchInDestructor instance created throw_int_func() invoked Catched int in ~ThrowCatchInDestructor(): 1 ~PrintInDestructor() invoked Catched const char* exception: BANG!
      
      





不快な状況を避ける方法は? 理論的には、すべてが単純です。デストラクタで例外をスローしないでください。 ただし、実際には、この単純な要件を美しくエレガントに実現することはそれほど単純ではありません。







できないが、本当にしたい場合...



すぐに、デストラクタから例外をスローすることを正当化しようとはしていません。Sutter、Meyers、およびその他のC ++の達人の後、これを絶対に行わないことをお勧めします(少なくとも新しいコードでは)。 それにもかかわらず、実際には、プログラマーはレガシーコードに遭遇する可能性が高く、これは高水準に導くのはそれほど容易ではありません。 さらに、以下で頻繁に説明する手法は、デバッグプロセス中に役立ちます。

たとえば、特定のリソースでの作業をカプセル化するラッパークラスを持つライブラリを開発しています。 RAIIの原則に従って、コンストラクタでリソースを取得し、デストラクタでリソースを解放する必要があります。 しかし、リソースを解放しようとして失敗した場合はどうなりますか? この問題の解決策は次のとおりです。









現在、例外によりスタックを昇格させているかどうかを理解する方法は? C ++には、 このための特別な関数std :: uncaught_exception()があります 。 その助けを借りて、通常の状況で安全に例外をスローしたり、より適切ではないがスタックプロモーション中に例外をスローしたりしないようにすることができます。







 class ThrowInDestructor { public: ~ThrowInDestructor() noexcept(false) { if (std::uncaught_exception()) { std::cerr << "~ThrowInDestructor() stack unwinding, not throwing\n"; } else { std::cerr << "~ThrowInDestructor() normal case, throwing\n"; throw_int_func(); } } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowInDestructor normal; std::cerr << "ThrowInDestructor normal destruction\n"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } try { ThrowInDestructor stack_unwind; std::cerr << "ThrowInDestructor stack unwinding\n"; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; }
      
      





結果:







 ThrowInDestructor normal destruction ~ThrowInDestructor() normal case, throwing throw_int_func() invoked ~PrintInDestructor() invoked Catched int exception: 1 ThrowInDestructor stack unwinding ~ThrowInDestructor() stack unwinding, not throwing ~PrintInDestructor() invoked Catched const char* exception: BANG!
      
      





std :: uncaught_exception()関数はC ++ Standard 17から非推奨になっていることに注意してください。したがって、例をコンパイルするには、対応する変動を抑制する必要があります記事の例のあるリポジトリを参照 )。







この関数の問題は、例外によってスタックを回転させているかどうかをチェックすることです。 ただし、スタックプロモーションのプロセス中に現在のデストラクタが呼び出されたかどうかを理解することはできません。 結果として、スタックの昇格はあるが、あるオブジェクトのデストラクタが通常に呼び出される場合、 std :: uncaught_exception()は依然としてtrueを返します







 class MayThrowInDestructor { public: ~MayThrowInDestructor() noexcept(false) { if (std::uncaught_exception()) { std::cerr << "~MayThrowInDestructor() stack unwinding, not throwing\n"; } else { std::cerr << "~MayThrowInDestructor() normal case, throwing\n"; throw_int_func(); } } }; class ThrowCatchInDestructor { public: ~ThrowCatchInDestructor() noexcept(false) { try { MayThrowInDestructor may_throw; } catch (int i) { std::cerr << "Catched int in ~ThrowCatchInDestructor(): " << i << "\n"; } } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowCatchInDestructor stack_unwind; std::cerr << "ThrowInDestructor stack unwinding\n"; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; }
      
      





結果:







 ThrowInDestructor stack unwinding ~MayThrowInDestructor() stack unwinding, not throwing ~PrintInDestructor() invoked Catched const char* exception: BANG!
      
      





新しいC ++ 17標準では、 std :: uncaught_exceptions()関数が、 std :: uncaught_exception() (複数形に注意を置き換えるために導入されました。







これは、上記の問題をstd :: uncaught_exceptions()で解決する方法です。







 class MayThrowInDestructor { public: MayThrowInDestructor() : exceptions_(std::uncaught_exceptions()) {} ~MayThrowInDestructor() noexcept(false) { if (std::uncaught_exceptions() > exceptions_) { std::cerr << "~MayThrowInDestructor() stack unwinding, not throwing\n"; } else { std::cerr << "~MayThrowInDestructor() normal case, throwing\n"; throw_int_func(); } } private: int exceptions_; }; class ThrowCatchInDestructor { public: ~ThrowCatchInDestructor() noexcept(false) { try { MayThrowInDestructor may_throw; } catch (int i) { std::cerr << "Catched int in ~ThrowCatchInDestructor(): " << i << "\n"; } } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowCatchInDestructor stack_unwind; std::cerr << "ThrowInDestructor stack unwinding\n"; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; }
      
      





結果:







 ThrowInDestructor stack unwinding ~MayThrowInDestructor() normal case, throwing throw_int_func() invoked Catched int in ~ThrowCatchInDestructor(): 1 ~PrintInDestructor() invoked Catched const char* exception: BANG!
      
      





本当にいくつかの例外を一度にスローしたい場合



std :: uncaught_exceptions()std :: terminate()の呼び出しを回避しますが、複数の例外を正しく処理するのに役立ちません。 理想的には、スローされたすべての例外を保存して、それらを1か所で処理できるメカニズムが必要です。







私が以下で提案するメカニズムは、概念を実証するためだけのものであり、実際の産業用コードでの使用は推奨されないことをもう一度思い出したいと思います。

考え方は、例外をキャッチしてコンテナに保存し、一度に1つずつ取得して処理することです。 例外オブジェクトを保存するために、C ++には特別な型std :: exception_ptrがあります。 標準の型構造は公開されていませんが、基本的に例外オブジェクトごとのshared_ptrと言われています。







これらの例外を処理する方法は? これを行うために、ポインタstd :: exception_ptrを受け取り、対応する例外をスローする関数std :: rethrow_exception()があります。 対応するキャッチセクションでキャッチして処理するだけで、その後、次の例外オブジェクトに移動できます。







 using exceptions_queue = std::stack<std::exception_ptr>; // Get exceptions queue for current thread exceptions_queue& get_queue() { thread_local exceptions_queue queue_; return queue_; } // Invoke functor and save exception in queue void safe_invoke(std::function<void()> f) noexcept { try { f(); } catch (...) { get_queue().push(std::current_exception()); } } class ThrowInDestructor { public: ~ThrowInDestructor() noexcept { std::cerr << "~ThrowInDestructor() invoked\n"; safe_invoke([]() { throw_int_func(); }); } private: PrintInDestructor member_; }; int main(int, char**) { safe_invoke([]() { ThrowInDestructor bad; throw "BANG!"; }); auto& q = get_queue(); while (!q.empty()) { try { std::exception_ptr ex = q.top(); q.pop(); if (ex != nullptr) { std::rethrow_exception(ex); } } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } } return 0; }
      
      





結果:







 ~ThrowInDestructor() invoked throw_int_func() invoked ~PrintInDestructor() invoked Catched const char* exception: BANG! Catched int exception: 1
      
      





上記の例では、スタックを使用して例外オブジェクトを保存しますが、FIFOの原則に従って例外処理が実行されます(つまり、論理的にこれはキューです-最初にスローされた例外が最初に処理されます)。







結論



オブジェクトデストラクターで例外をスローするのは本当に悪い考えです。新しいコードでは、 noexceptデストラクターを宣言してこれを行わないことを強くお勧めします。 ただし、レガシーコードのサポートとデバッグでは、スタックプロモーション中を含め、デストラクタからスローされた例外を正しく処理する必要がある場合があり、最新のC ++はこのためのメカニズムを提供します。 記事で提示されたアイデアが、この困難な道のりに役立つことを願っています。







参照資料



記事のサンプルを含むリポジトリ








All Articles