简体   繁体   English

为什么C ++不使用std :: nested_exception来允许从析构函数中抛出?

[英]Why doesn't C++ use std::nested_exception to allow throwing from destructor?

The main problem with throwing exceptions from destructor is that in the moment when destructor is called another exception may be "in flight" ( std::uncaught_exception() == true ) and so it is not obvious what to do in that case. 从析构函数抛出异常的主要问题是,在析构函数被调用的那一刻,另一个异常可能是“在飞行中”( std::uncaught_exception() == true ),因此在这种情况下做什么并不明显。 "Overwriting" the old exception with the new one would be the one of the possible ways to handle this situation. 用新的“覆盖”旧的异常将是处理这种情况的可能方法之一。 But it was decided that std::terminate (or another std::terminate_handler ) must be called in such cases. 但是决定在这种情况下必须调用std::terminate (或另一个std::terminate_handler )。

C++11 introduced nested exceptions feature via std::nested_exception class. C ++ 11通过std::nested_exception类引入了嵌套异常功能。 This feature could be used to solve the problem described above. 该特征可用于解决上述问题。 The old (uncaught) exception could be just nested into the new exception (or vice versa?) and then that nested exception could be thrown. 旧(未捕获)异常可能只是嵌套到新异常中(反之亦然?)然后可能抛出嵌套异常。 But this idea was not used. 但是没有使用这个想法。 std::terminate is still called in such situation in C++11 and C++14. 在C ++ 11和C ++ 14中仍然会调用std::terminate

So the questions. 所以问题。 Was the idea with nested exceptions considered? 是否考虑过嵌套异常的想法? Are there any problems with it? 它有什么问题吗? Isn't the situation going to be changed in the C++17? 是不是在C ++ 17中会改变这种情况?

The problem you cite happens when your destructor is being executed as part of the stack unwinding process (when your object was not created as part of stack unwinding) 1 , and your destructor needs to emit an exception. 当您的析构函数作为堆栈展开过程的一部分执行时(当您的对象未作为堆栈展开的一部分创建时),您引用的问题发生在1 ,并且您的析构函数需要发出异常。

So how does that work? 那怎么办? You have two exceptions in play. 你有两个例外。 Exception X is the one that's causing the stack to unwind. 异常X是导致堆栈展开的异常。 Exception Y is the one that the destructor wants to throw. 异常Y是析构函数想要抛出的异常。 nested_exception can only hold one of them. nested_exception只能包含其中一个

So maybe you have exception Y contain a nested_exception (or maybe just an exception_ptr ). 所以也许你有异常Y 包含一个nested_exception (或者只是一个exception_ptr )。 So... how do you deal with that at the catch site? 所以...你如何处理catch网站?

If you catch Y , and it happens to have some embedded X , how do you get it? 如果你抓到Y ,并且碰巧有一些嵌入式X ,你怎么得到它? Remember: exception_ptr is type-erased ; 请记住: exception_ptr类型擦除的 ; aside from passing it around, the only thing you can do with it is rethrow it. 除了传递它,你唯一可以做的就是重新抛出它。 So should people be doing this: 那么人们应该这样做:

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

I don't see a lot of people doing that. 我没有看到很多人这样做。 Especially since there would be an exceedingly large number of possible X -es. 特别是因为可能存在极大数量的X -es。

1 : Please do not use std::uncaught_exception() == true to detect this case. 1 :请不要使用std::uncaught_exception() == true来检测这种情况。 It is extremely flawed. 这是非常有缺陷的。

There is one use for std::nested exception , and only one use (as far as I have been able to discover). std::nested exception有一个用途,只有一个用途(据我所知)。

Having said that, it's fantastic, I use nested exceptions in all my programs and as a result the time spent hunting obscure bugs is almost zero. 话虽如此,它太棒了,我在所有程序中都使用嵌套异常,因此花费时间来搜寻晦涩的bug几乎为零。

This is because nesting exceptions allow you to easily build a fully-annotated call stack which is generated at the point of the error, without any runtime overhead, no need for copious logging during a re-run (which will change the timing anyway), and without polluting program logic with error handling. 这是因为嵌套异常允许您轻松构建在错误点生成的完全注释的调用堆栈,没有任何运行时开销,在重新运行期间不需要大量日志记录(这将改变时序),并且没有污染程序逻辑和错误处理。

for example: 例如:

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

expected output: 预期产量:

exception: outer : abcd
exception:  inner : abcdabcd
exception:   really_inner
exception:    too long

Explanation of the expander line for @Xenial: @Xenial扩展行的说明:

void (expand{ 0, ((ss << sep << args), sep = ", ", 0)... });

args is a parameter pack. args是一个参数包。 It represents 0 or more arguments (the zero is important). 它代表0个或多个参数(零很重要)。

What we're looking to do is to get the compiler to expand the argument pack for us while writing useful code around it. 我们要做的是让编译器为我们扩展参数包,同时围绕它编写有用的代码。

Let's take it from outside in: 让我们从外面看:

void(...) - means evaluate something and throw away the result - but do evaluate it. void(...) - 表示评估某些东西并丢弃结果 - 但要评估它。

expand{ ... };

Remembering that expand is a typedef for int[], this means let's evaluate an integer array. 记住expand是int []的typedef,这意味着让我们计算一个整数数组。

0, (...)...;

means the first integer is zero - remember that in c++ it's illegal to define a zero-length array. 意味着第一个整数为零 - 请记住,在c ++中定义零长度数组是非法的。 What if args... represents 0 parameters? 如果args ...代表0参数怎么办? This 0 ensures that the array has at lease one integer in it. 这个0确保数组中至少有一个整数。

(ss << sep << args), sep = ", ", 0);

uses the comma operator to evaluate a sequence of expressions in order, taking the result of the last one. 使用逗号运算符按顺序计算表达式序列,获取最后一个表达式的结果。 The expressions are: 表达式是:

s << sep << args - print the separator followed by the current argument to the stream s << sep << args - 打印分隔符,后跟流的当前参数

sep = ", " - then make the separator point to a comma + space sep = ", " - 然后使分隔符指向逗号+空格

0 - result in the value 0. This is the value that goes in the array. 0 - 结果为0.这是数组中的值。

(xxx params yyy)... - means do this once for each parameter in the parameter pack params (xxx params yyy)... -用于在参数组中的每个参数做一次params

Therefore: 因此:

void (expand{ 0, ((ss << sep << args), sep = ", ", 0)... });

means "for every parameter in params, print it to ss after printing the separator. Then update the separator (so that we have a different separator for the first one). Do all this as part of initialising an imaginary array which we will then throw away. 表示“对于params中的每个参数,在打印分隔符后将其打印到ss。然后更新分隔符(以便我们为第一个分隔符设置不同的分隔符)。将所有这些作为初始化假想数组的一部分,然后我们将抛出远。

Nested exceptions just add most-likely-ignored information about what happened, which is this: 嵌套异常只会添加最可能被忽略的有关所发生事件的信息,这是:

An exception X has been thrown, the stack is being unwound, ie destructors of local objects are being called with that exception “in flight”, and the destructor of one of those objects in turn throws an exception Y. 抛出异常X,堆栈正在展开,即在“飞行中”异常调用本地对象的析构函数,其中一个对象的析构函数依次抛出异常Y.

Ordinarily this means that cleanup failed. 通常这意味着清理失败。

And then this is not a failure that can be remedied by reporting it upwards and letting higher level code decide to eg use some alternative means to achieve its goal, because the object that held the information necessary to do the clean up has been destroyed , along with its information, but without doing its cleanup. 然后,这不是一个可以通过向上报告并让更高级别的代码决定使用一些替代方法来实现其目标来弥补的失败,因为持有清理所需信息的对象已被破坏 ,有了它的信息,但没有进行清理。 So it's much like an assertion failing. 所以它就像一个断言失败。 The process state can be very ungood, breaking the assumptions of the code. 进程状态可能非常糟糕,打破了代码的假设。

Destructors that throw can in principle be useful, eg as the idea Andrei once aired about indicating a failed transaction on exit from a block scope. 抛出的析构函数原则上可以是有用的,例如,Andrei曾经提出过关于在块范围退出时指示失败事务的想法。 That is, in normal code execution a local object that hasn't been informed of transaction success can throw from its destructor. 也就是说,在正常的代码执行中,未被告知事务成功的本地对象可以从其析构函数中抛出。 This only becomes a problem when it clashes with C++'s rule for exception during stack unwinding, where it requires detection of whether the exception can be thrown, which appears to be impossible. 在堆栈展开期间它与C ++的异常规则冲突时,这只会成为一个问题,它需要检测是否可以抛出异常,这似乎是不可能的。 Anyway then the destructor is being used just for its automatic call, not in its cleanup rôle. 无论如何,析构函数仅用于其自动调用,而不是用于清理rôle。 And so one can conclude that the current C++ rules assume the cleanup rôle for destructors. 因此可以得出结论,当前的C ++规则假定了析构函数的清理工作。

The real problem is that throwing from destructors is a logical fallacy. 真正的问题是从析构函数中抛出是一个逻辑上的谬误。 It's like defining operator+() to perform multiplication. 这就像定义operator +()来执行乘法。 Destructors should not be used as hooks for running arbitrary code. 不应将析构函数用作运行任意代码的钩子。 Their purpose is to deterministically release resources. 他们的目的是确定性地释放资源。 By definition, that must not fail. 根据定义,这绝不能失败。 Anything else breaks the assumptions needed to write generic code. 其他任何东西都打破了编写通用代码所需的假设。

The problem that may happen during stack unwinding with chaining exceptions from destructors is that the nested exception chain may be too long. 在使用析构函数的链接异常进行堆栈展开期间可能发生的问题是嵌套的异常链可能太长。 For example, you have std::vector of 1 000 000 elements each of which throws an exception in its destructor. 例如,你有1 000 000元素的std::vector ,每个元素在它的析构函数中抛出一个异常。 Let's assume the destructor of std::vector collects all exceptions from destructors of its elements into single chain of nested exceptions. 让我们假设std::vector析构函数将其元素的析构函数中的所有异常收集到单个嵌套异常链中。 Then resulting exception may be even bigger than original std::vector container. 然后导致的异常甚至可能比原始的std::vector容器更大。 This may cause performance problems and even throwing std::bad_alloc during stack unwinding (that even couldn't be nested because there is not enough memory for doing that) or throwing std::bad_alloc in other unrelated places in the program. 这可能会导致性能问题,甚至在堆栈展开期间抛出std::bad_alloc (甚至无法嵌套,因为没有足够的内存来执行此操作)或者在程序中的其他无关位置抛出std::bad_alloc

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM