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

新手上路,请多包涵

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

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

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

原文由 anton_rh 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 527
2 个回答

您引用的问题发生在您的析构函数作为堆栈展开过程的一部分执行时(当您的对象不是作为堆栈展开的一部分创建时) 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 来检测这种情况。这是非常有缺陷的。

原文由 Nicol Bolas 发布,翻译遵循 CC BY-SA 3.0 许可协议

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。然后更新分隔符(以便我们为第一个分隔符使用不同的分隔符)。将所有这些作为初始化虚构数组的一部分,然后我们将抛出离开。

原文由 Richard Hodges 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题