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

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

从析构函数中抛出异常的主要问题是,在调用析构函数的时候,另一个异常可能是"正在运行"(std::uncaught_exception() == true),因此在这种情况下不太明显该怎么做。用新的例外重写旧的例外是处理这种情况的可能方法之一。但决定在这种情况下必须调用std::terminate(或另一个std::terminate_handler)。

C++ 11通过EDCOX1×3类引入嵌套异常特征。此功能可用于解决上述问题。旧的(未捕获的)异常可以嵌套到新的异常中(反之亦然?)然后可以抛出嵌套的异常。但这个想法没有被使用。在C++ 11和C++ 14的这种情况下,仍然调用std::terminate

所以问题是。是否考虑了嵌套异常的想法?有什么问题吗?在C++ 17中情况不会改变吗?


当析构函数作为堆栈展开过程的一部分执行时(当对象不是作为堆栈展开过程的一部分创建时),您引用的问题会发生1,并且析构函数需要发出异常。

那么,这是如何工作的呢?你有两个例外。异常X是导致堆栈展开的异常。异常Y是析构函数想要抛出的异常。nested_exception只能容纳其中一个。

所以,也许你有一个例外:Y包含一个nested_exception,或者只是一个exception_ptr。所以…你在catch网站上是如何处理的?

如果你抓到了Y,而它恰好有一些嵌入的X,你是怎么得到它的?记住:exception_ptr是一个类型删除,除了传递它,你唯一能做的就是重新发送它。所以人们应该这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
catch(Y &e)
{
  if(e.has_nested())
  {
    try
    {
      e.rethrow_nested();
    }
    catch(X &e2)
    {
    }
  }
}

我没看到很多人这样做。尤其是因为可能存在大量的X-es。

1:请不要用std::uncaught_exception() == true来检测这个病例。它有极大的缺陷。


std::nested exception有一个用途,只有一个用途(据我所知)。

说了这句话,我在所有程序中都使用嵌套的异常,因此花在搜索不明显的bug上的时间几乎为零。

这是因为嵌套异常允许您轻松构建在错误点生成的完全注释的调用堆栈,而不需要任何运行时开销,也不需要在重新运行期间进行大量的日志记录(这无论如何都会改变计时),并且不必通过错误处理污染程序逻辑。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#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() << '
'
;
    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;
}

预期输出:

1
2
3
4
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{ ... };

记住EDOCX1[4]是int[]的typedef,这意味着让我们计算一个整数数组。

0, (...)...;

意味着第一个整数为零——记住C++中定义零长度数组是非法的。如果args…表示0个参数?此0确保数组中至少有一个整数。

6

使用逗号运算符按顺序计算表达式序列,并获取最后一个表达式的结果。表达式为:

s << sep << args-打印分隔符,后跟流的当前参数

sep =","—然后使分隔符指向逗号+空格

0—结果为0。这是数组中的值。

(xxx params yyy)...表示对参数包params中的每个参数执行一次。

因此:

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

表示"对于参数中的每个参数,打印分隔符后将其打印到ss。然后更新分隔符(以便第一个分隔符的分隔符不同)。做所有这些,作为初始化一个虚拟数组的一部分,然后我们将丢弃它。


嵌套异常只会添加最可能被忽略的有关发生的情况的信息,即:

引发了异常X,堆栈正在解除绑定,即调用本地对象的析构函数时出现异常&ldquo;in flight&rdquo;,而其中一个对象的析构函数又抛出异常Y。

通常这意味着清理失败。

然后,这不是一个可以通过向上报告并让更高级别的代码决定(例如,使用其他方法来实现其目标)来补救的失败,因为保存清理所需信息的对象已经与其信息一起被销毁,但没有进行清理。所以这很像一个失败的断言。进程状态可能非常不正常,违反了代码的假设。

原则上,抛出的析构函数是有用的,例如Andrei曾经提出过在块作用域退出时指示失败事务的想法。也就是说,在正常的代码执行中,没有被通知事务成功的本地对象可以从其析构函数中抛出。这只会成为一个问题,当它在堆栈展开时与C++规则冲突时,它需要检测是否可以抛出异常,这看起来是不可能的。不管怎样,析构函数只是用于它的自动调用,而不是在它的清理程序r中?乐。因此,可以得出结论,当前的C++规则假设了清理R?对于析构函数。


通过从析构函数链接异常的堆栈展开过程中可能发生的问题是嵌套异常链可能太长。例如,您有1 000 000元素的std::vector,每个元素都在其析构函数中抛出异常。假设std::vector的析构函数将其元素的析构函数中的所有异常收集到嵌套异常的单链中。那么产生的异常可能比原始的std::vector容器还要大。这可能会导致性能问题,甚至在堆栈展开期间抛出std::bad_alloc(因为没有足够的内存来进行此操作,所以甚至不能嵌套)或在程序中其他不相关的地方抛出std::bad_alloc


真正的问题是从析构函数中抛出是一个逻辑谬论。这就像定义运算符+()来执行乘法。析构函数不应用作运行任意代码的钩子。它们的目的是确定地释放资源。根据定义,这一定不会失败。其他任何东西都会破坏编写通用代码所需的假设。