22

从析构函数抛出异常的主要问题是,在调用析构函数的那一刻,另一个异常可能是“在飞行中”(std::uncaught_exception() == true),因此在这种情况下该怎么做并不明显。用新异常“覆盖”旧异常将是处理这种情况的可能方法之一。但是决定在这种情况下必须调用std::terminate(或另一个)。std::terminate_handler

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

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

4

5 回答 5

25

有一种用途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{ ... };

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

0, (...)...;

表示第一个整数为零 - 请记住,在 c++ 中定义零长度数组是非法的。如果 args... 代表 0 个参数怎么办?这个 0 确保数组中至少有一个整数。

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

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

s << sep << args- 将分隔符后跟当前参数打印到流中

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

0- 产生值 0。这是数组中的值。

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

所以:

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

表示“对于 params 中的每个参数,在打印分隔符后将其打印到 ss。然后更新分隔符(以便我们为第一个分隔符使用不同的分隔符)。将所有这些作为初始化虚构数组的一部分,然后我们将抛出离开。

于 2016-05-14T14:32:15.460 回答
9

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

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

所以也许你有异常Y 包含一个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。

1:请不要std::uncaught_exception() == true用于检测这种情况。这是非常有缺陷的。

于 2016-05-14T13:55:05.587 回答
2

嵌套异常只是添加最有可能被忽略的关于发生了什么的信息,即:

异常 X 已被抛出,堆栈正在展开,即本地对象的析构函数被调用,该异常“正在运行”,而其中一个对象的析构函数又抛出异常 Y。

通常这意味着清理失败。

然后这不是一个可以通过向上报告并让更高级别的代码决定例如使用一些替代方法来实现其目标来补救的故障,因为持有进行清理所需信息的对象已被破坏,随着用它的信息,但没有做它的清理。所以这很像一个断言失败。进程状态可能非常糟糕,破坏​​了代码的假设。

throw 的析构函数原则上是有用的,例如 Andrei 曾经提出过在退出块作用域时指示失败的事务的想法。也就是说,在正常的代码执行中,没有被告知事务成功的本地对象可以从其析构函数中抛出。只有当它在堆栈展开期间与 C++ 的异常规则发生冲突时才会成为问题,在该规则中,它需要检测是否可以抛出异常,这似乎是不可能的。无论如何,析构函数仅用于其自动调用,而不是用于其清理角色。因此可以得出结论,当前的 C++ 规则承担了析构函数的清理角色。

于 2016-05-14T13:55:15.323 回答
1

在使用来自析构函数的链接异常的堆栈展开期间可能发生的问题是嵌套的异常链可能太长。例如,您有std::vector一个1 000 000元素,每个元素都会在其析构函数中引发异常。让我们假设析std::vector构函数将其元素的析构函数中的所有异常收集到单个嵌套异常链中。然后产生的异常可能比原始std::vector容器更大。这可能会导致性能问题,甚至std::bad_alloc在堆栈展开期间抛出(甚至无法嵌套,因为没有足够的内存来执行此操作)或抛出std::bad_alloc程序中其他不相关的位置。

于 2016-05-15T03:23:56.250 回答
1

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

于 2016-05-14T19:03:23.603 回答