22

デストラクタから例外をスローする際の主な問題は、デストラクタが呼び出された瞬間に別の例外が「飛行中」( std::uncaught_exception() == true) である可能性があるため、その場合に何をすべきかが明らかではないことです。古い例外を新しい例外で「上書き」することは、この状況を処理する可能な方法の 1 つです。しかし、そのような場合はstd::terminate(または別のstd::terminate_handler) を呼び出さなければならないことが決定されました。

C++11 では、std::nested_exceptionクラスを介してネストされた例外機能が導入されました。この機能は、上記の問題を解決するために使用できます。古い (キャッチされていない) 例外を新しい例外にネストするだけで (またはその逆?)、ネストされた例外をスローすることができます。しかし、このアイデアは使用されませんでした。std::terminateC++11 および C++14 では、このような状況でも呼び出されます。

だから質問。ネストされた例外を含むアイデアは考慮されましたか? 問題はありますか?C++17で状況が変わるんじゃないの?

4

5 に答える 5

25

には 1 つの用途があり、その用途は 1 つstd::nested exceptionだけです (私が発見できた限り)。

そうは言っても、素晴らしいことに、私はすべてのプログラムでネストされた例外を使用しています。その結果、あいまいなバグを探すのに費やす時間はほとんどゼロです。

これは、ネストされた例外により、エラーの時点で生成される完全に注釈が付けられたコール スタックを簡単に構築できるためです。実行時のオーバーヘッドはなく、再実行中に大量のログを記録する必要もありません (とにかくタイミングが変わります)。エラー処理でプログラム ロジックを汚染することもありません。

例えば:

#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{ ... };

これは int[] の typedef であることを思い出してください。つまりexpand、整数配列を評価してみましょう。

0, (...)...;

最初の整数が 0 であることを意味します。C++ では、長さ 0 の配列を定義することは違法であることを思い出してください。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に出力します。次に、セパレーターを更新します(最初のセパレーターに別のセパレーターを持たせるため)。これらすべてを、次にスローする架空の配列の初期化の一部として行います。あちらへ。

于 2016-05-14T14:32:15.460 に答える
9

あなたが引用した問題は、デストラクタがスタックの巻き戻しプロセスの一部として実行されているときに発生し (スタックの巻き戻しの一部としてオブジェクトが作成されなかった場合) 1、デストラクタが例外を発行する必要があります。

それで、それはどのように機能しますか?あなたには 2 つの例外があります。例外Xは、スタックの巻き戻しを引き起こしているものです。例外Yは、デストラクタがスローしたいものです。1nested_exceptionつしか保持できません。

したがって、例外Y a nested_exception(または単にexception_ptr) が含まれている可能性があります。では、現場ではどのように対処していますcatchか?

をキャッチYし、たまたま が埋め込まれている場合X、どうやってそれを取得しますか? 覚えておいてください:タイプ消去さexception_ptrれます。渡す以外にできることは、再スローすることだけです。したがって、人々はこれを行う必要があります:

catch(Y &e)
{
  if(e.has_nested())
  {
    try
    {
      e.rethrow_nested();
    }
    catch(X &e2)
    {
    }
  }
}

そうしている人はあまり見かけません。特に可能なX-es の数が非常に多いためです。

1std::uncaught_exception() == true :この場合の検出には使用しないでください。それは非常に欠陥があります。

于 2016-05-14T13:55:05.587 に答える
2

ネストされた例外は、何が起こったのかについて無視される可能性が最も高い情報を追加するだけです。これは次のとおりです。

例外 X がスローされ、スタックがアンワインドされています。つまり、ローカル オブジェクトのデストラクタがその例外を「実行中」で呼び出され、それらのオブジェクトの 1 つのデストラクタが例外 Y をスローします。

通常、これはクリーンアップが失敗したことを意味します。

そして、これは、それを上に報告し、より高いレベルのコードに、たとえば目標を達成するために何らかの代替手段を使用することを決定させることによって修復できる失敗ではありませんその情報を使用しますが、クリーンアップは行いません。したがって、アサーションの失敗によく似ています。プロセスの状態が非常に悪く、コードの前提が崩れる可能性があります。

例外をスローするデストラクタは、原則として有用です。たとえば、Andrei がブロック スコープからの終了時に失敗したトランザクションを示すことについて放映したアイデアのように。つまり、通常のコード実行では、トランザクションの成功が通知されていないローカル オブジェクトがデストラクタからスローされる可能性があります。これは、例外をスローできるかどうかを検出する必要があるスタックの巻き戻し中に C++ の例外規則と衝突する場合にのみ問題になりますが、これは不可能に見えます。とにかく、デストラクタはクリーンアップの役割ではなく、自動呼び出しのためだけに使用されています。したがって、現在の C++ ルールは、デストラクタのクリーンアップの役割を想定していると結論付けることができます。

于 2016-05-14T13:55:15.323 に答える
1

デストラクタからの連鎖例外を伴うスタックの巻き戻し中に発生する可能性がある問題は、ネストされた例外連鎖が長すぎる可能性があることです。たとえば、それぞれstd::vector1 000 000デストラクタで例外をスローする要素があります。のstd::vectorデストラクタが、その要素のデストラクタからすべての例外を、ネストされた例外の 1 つのチェーンに収集するとします。結果として生じる例外は、元のstd::vectorコンテナーよりもさらに大きくなる可能性があります。これにより、パフォーマンスの問題が発生したりstd::bad_alloc、スタックの巻き戻し中にスローしたり (ネストするのに十分なメモリがないためにネストできなかった) std::bad_alloc、プログラム内の他の無関係な場所でスローしたりする可能性があります。

于 2016-05-15T03:23:56.250 に答える
1

本当の問題は、デストラクタからのスローが論理的な誤謬であることです。乗算を実行するために operator+() を定義するようなものです。デストラクタは、任意のコードを実行するためのフックとして使用しないでください。それらの目的は、決定論的にリソースを解放することです。定義上、それは失敗してはなりません。それ以外のものは、一般的なコードを書くために必要な仮定を破ります。

于 2016-05-14T19:03:23.603 に答える