デストラクタから例外をスローする際の主な問題は、デストラクタが呼び出された瞬間に別の例外が「実行中」(std::uncaught_exception() == true
)になる可能性があるため、その場合の対処方法が明確でないことです。古い例外を新しい例外で「上書き」することは、この状況を処理するための可能な方法の1つです。しかし、そのような場合はstd::terminate
(または別のstd::terminate_handler
)を呼び出す必要があることが決定されました。
C++ 11では、std::nested_exception
クラスを介してネストされた例外機能が導入されました。この機能は、上記の問題を解決するために使用できます。古い(キャッチされていない)例外を新しい例外にネストするだけで(またはその逆)、ネストされた例外をスローできます。しかし、このアイデアは使用されませんでした。 std::terminate
は、C++ 11およびC++ 14でもこのような状況で呼び出されます。
だから質問。ネストされた例外を含むアイデアは考慮されましたか?何か問題はありますか? C++ 17では状況が変わるのではないですか?
あなたが引用する問題は、デストラクタがスタック巻き戻しプロセスの一部として実行されているときに発生します(オブジェクトがスタック巻き戻しの一部として作成されていない場合)1、およびデストラクタは例外を発行する必要があります。
では、それはどのように機能しますか?プレイには2つの例外があります。例外X
は、スタックの巻き戻しを引き起こしているものです。例外Y
は、デストラクタがスローしたいものです。 _nested_exception
_はそれらのうちoneのみを保持できます。
したがって、例外Y
contain a _nested_exception
_(または単に_exception_ptr
_)があるかもしれません。では...catch
サイトでどのように対処しますか?
Y
をキャッチし、X
が埋め込まれている場合、どのようにして取得しますか?覚えておいてください:_exception_ptr
_はtype-erased;それを渡すことを除いて、あなたがそれでできる唯一のことはそれを投げ直すことです。だから人々はこれをしているべきです:
_catch(Y &e)
{
if(e.has_nested())
{
try
{
e.rethrow_nested();
}
catch(X &e2)
{
}
}
}
_
そんなことをしている人はあまりいません。特に、可能性のあるX
-esの数が非常に多いためです。
1:このケースを検出するためにstd::uncaught_exception() == true
を使用しないでください。それは非常に欠陥があります。
_std::nested exception
_には1つの用途があり、(私が発見できた限りでは)1つの用途しかありません。
そうは言っても、それは素晴らしいことです。私はすべてのプログラムでネストされた例外を使用しているため、あいまいなバグを探すのに費やす時間はほぼゼロです。
これは、ネスト例外を使用すると、実行時のオーバーヘッドがなく、再実行中に大量のログを記録する必要がなく、エラーの時点で生成される完全に注釈が付けられたコールスタックを簡単に構築できるためです(とにかくタイミングが変更されます)。エラー処理でプログラムロジックを汚染することなく。
例えば:
_#include <iostream>
#include <exception>
#include <stdexcept>
#include <sstream>
#include <string>
// this function will re-throw the current exception, nested inside a
// new one. If the std::current_exception is derived from logic_error,
// this function will throw a logic_error. Otherwise it will throw a
// runtime_error
// The message of the exception will be composed of the arguments
// context and the variadic arguments args... which may be empty.
// The current exception will be nested inside the new one
// @pre context and args... must support ostream operator <<
template<class Context, class...Args>
void rethrow(Context&& context, Args&&... args)
{
// build an error message
std::ostringstream ss;
ss << context;
auto sep = " : ";
using expand = int[];
void (expand{ 0, ((ss << sep << args), sep = ", ", 0)... });
// figure out what kind of exception is active
try {
std::rethrow_exception(std::current_exception());
}
catch(const std::invalid_argument& e) {
std::throw_with_nested(std::invalid_argument(ss.str()));
}
catch(const std::logic_error& e) {
std::throw_with_nested(std::logic_error(ss.str()));
}
// etc - default to a runtime_error
catch(...) {
std::throw_with_nested(std::runtime_error(ss.str()));
}
}
// unwrap nested exceptions, printing each nested exception to
// std::cerr
void print_exception (const std::exception& e, std::size_t depth = 0) {
std::cerr << "exception: " << std::string(depth, ' ') << e.what() << '\n';
try {
std::rethrow_if_nested(e);
} catch (const std::exception& nested) {
print_exception(nested, depth + 1);
}
}
void really_inner(std::size_t s)
try // function try block
{
if (s > 6) {
throw std::invalid_argument("too long");
}
}
catch(...) {
rethrow(__func__); // rethrow the current exception nested inside a diagnostic
}
void inner(const std::string& s)
try
{
really_inner(s.size());
}
catch(...) {
rethrow(__func__, s); // rethrow the current exception nested inside a diagnostic
}
void outer(const std::string& s)
try
{
auto cpy = s;
cpy.append(s.begin(), s.end());
inner(cpy);
}
catch(...)
{
rethrow(__func__, s); // rethrow the current exception nested inside a diagnostic
}
int main()
{
try {
// program...
outer("xyz");
outer("abcd");
}
catch(std::exception& e)
{
// ... why did my program fail really?
print_exception(e);
}
return 0;
}
_
期待される出力:
_exception: outer : abcd
exception: inner : abcdabcd
exception: really_inner
exception: too long
_
@Xenialのエキスパンダーラインの説明:
void (expand{ 0, ((ss << sep << args), sep = ", ", 0)... });
argsはパラメータパックです。 0個以上の引数を表します(ゼロが重要です)。
私たちが探しているのは、コンパイラーに引数パックを拡張させながら、その周りに役立つコードを書くことです。
外からそれを取りましょう:
void(...)
-何かを評価して結果を破棄することを意味します-しかし、それを評価します。
_expand{ ... };
_
expand
はint []のtypedefであることを思い出してください。これは、整数配列を評価することを意味します。
0, (...)...;
最初の整数がゼロであることを意味します-C++では長さゼロの配列を定義することは違法であることに注意してください。 args ...が0個のパラメーターを表す場合はどうなりますか?この0は、配列に少なくとも1つの整数が含まれていることを保証します。
_(ss << sep << args), sep = ", ", 0);
_
コンマ演算子を使用して、式のシーケンスを順番に評価し、最後の式の結果を取得します。式は次のとおりです。
_s << sep << args
_-区切り文字の後にストリームへの現在の引数を出力します
_sep = ", "
_-次に、区切り文字がコンマ+スペースを指すようにします
_0
_-結果は値0になります。これは配列に入る値です。
_(xxx params yyy)...
_-パラメータパック内のパラメータごとにこれを1回実行することを意味しますparams
したがって:
void (expand{ 0, ((ss << sep << args), sep = ", ", 0)... });
意味「paramsのすべてのパラメーターについて、セパレーターを出力した後、ssに出力します。次にセパレーターを更新します(最初のパラメーターとは異なるセパレーターを使用します)。これはすべて、仮想配列の初期化の一部として実行し、次にスローします。離れて。
ネストされた例外は、何が起こったかについて最も無視される可能性が高い情報を追加するだけです。これは次のとおりです。
例外Xがスローされ、スタックがアンワインドされています。つまり、ローカルオブジェクトのデストラクタが「実行中」の例外で呼び出され、それらのオブジェクトの1つのデストラクタが例外Yをスローします。
通常、これはクリーンアップが失敗したことを意味します。
そして、これはそれを上向きに報告し、より高いレベルのコードに例えばクリーンアップを実行するために必要な情報を保持していたオブジェクトが破棄された、その情報とともに、ただしクリーンアップを実行しないため、目標を達成するためにいくつかの代替手段を使用します。つまり、アサーションが失敗するようなものです。プロセスの状態は非常に悪く、コードの前提を破ることがあります。
スローするデストラクタは、原則として便利です。 Andreiがかつて、ブロックスコープからの終了時に失敗したトランザクションを示すことについて放映されたアイデアとして。つまり、通常のコード実行では、トランザクションの成功が通知されていないローカルオブジェクトがデストラクタからスローされる可能性があります。これは、スタックの巻き戻し中に例外に関するC++のルールと衝突する場合にのみ問題になります。この場合、例外をスローできるかどうかの検出が必要ですが、これは不可能と思われます。とにかく、デストラクタは、クリーンアップロールではなく、自動呼び出しのためだけに使用されています。したがって、現在のC++ルールはデストラクタのクリーンアップロールを想定していると結論付けることができます。
デストラクタからのチェーン例外を使用したスタックの巻き戻し中に発生する可能性のある問題は、ネストされた例外チェーンが長すぎる可能性があることです。たとえば、std::vector
個の1 000 000
要素があり、それぞれがデストラクタで例外をスローします。 std::vector
のデストラクタが、その要素のデストラクタからネストされた例外の単一チェーンにすべての例外を収集するとします。その場合、結果として生じる例外は、元のstd::vector
コンテナよりもさらに大きくなる可能性があります。これにより、パフォーマンスの問題が発生したり、スタックの巻き戻し中にstd::bad_alloc
をスローしたり(メモリが不足しているためネストできなかった場合でも)、プログラム内の他の無関係な場所にstd::bad_alloc
をスローしたりする可能性があります。
本当の問題は、デストラクタからのスローは論理的な誤謬であるということです。これは、乗算を実行するためにoperator +()を定義するようなものです。デストラクタは、任意のコードを実行するためのフックとして使用しないでください。それらの目的は、リソースを決定論的に解放することです。定義上、それは失敗してはなりません。それ以外のものは、ジェネリックコードを書くために必要な仮定を破ります。