[英]Why doesn't C++ use std::nested_exception to allow throwing from destructor?
从析构函数抛出异常的主要问题是,在析构函数被调用的那一刻,另一个异常可能是“在飞行中”( std::uncaught_exception() == true
),因此在这种情况下做什么并不明显。 用新的“覆盖”旧的异常将是处理这种情况的可能方法之一。 但是决定在这种情况下必须调用std::terminate
(或另一个std::terminate_handler
)。
C ++ 11通过std::nested_exception
类引入了嵌套异常功能。 该特征可用于解决上述问题。 旧(未捕获)异常可能只是嵌套到新异常中(反之亦然?)然后可能抛出嵌套异常。 但是没有使用这个想法。 在C ++ 11和C ++ 14中仍然会调用std::terminate
。
所以问题。 是否考虑过嵌套异常的想法? 它有什么问题吗? 是不是在C ++ 17中会改变这种情况?
当您的析构函数作为堆栈展开过程的一部分执行时(当您的对象未作为堆栈展开的一部分创建时),您引用的问题发生在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
来检测这种情况。 这是非常有缺陷的。
std::nested exception
有一个用途,只有一个用途(据我所知)。
话虽如此,它太棒了,我在所有程序中都使用嵌套异常,因此花费时间来搜寻晦涩的bug几乎为零。
这是因为嵌套异常允许您轻松构建在错误点生成的完全注释的调用堆栈,没有任何运行时开销,在重新运行期间不需要大量日志记录(这将改变时序),并且没有污染程序逻辑和错误处理。
例如:
#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。然后更新分隔符(以便我们为第一个分隔符设置不同的分隔符)。将所有这些作为初始化假想数组的一部分,然后我们将抛出远。
嵌套异常只会添加最可能被忽略的有关所发生事件的信息,这是:
抛出异常X,堆栈正在展开,即在“飞行中”异常调用本地对象的析构函数,其中一个对象的析构函数依次抛出异常Y.
通常这意味着清理失败。
然后,这不是一个可以通过向上报告并让更高级别的代码决定使用一些替代方法来实现其目标来弥补的失败,因为持有清理所需信息的对象已被破坏 ,有了它的信息,但没有进行清理。 所以它就像一个断言失败。 进程状态可能非常糟糕,打破了代码的假设。
抛出的析构函数原则上可以是有用的,例如,Andrei曾经提出过关于在块范围退出时指示失败事务的想法。 也就是说,在正常的代码执行中,未被告知事务成功的本地对象可以从其析构函数中抛出。 在堆栈展开期间它与C ++的异常规则冲突时,这只会成为一个问题,它需要检测是否可以抛出异常,这似乎是不可能的。 无论如何,析构函数仅用于其自动调用,而不是用于清理rôle。 因此可以得出结论,当前的C ++规则假定了析构函数的清理工作。
真正的问题是从析构函数中抛出是一个逻辑上的谬误。 这就像定义operator +()来执行乘法。 不应将析构函数用作运行任意代码的钩子。 他们的目的是确定性地释放资源。 根据定义,这绝不能失败。 其他任何东西都打破了编写通用代码所需的假设。
在使用析构函数的链接异常进行堆栈展开期间可能发生的问题是嵌套的异常链可能太长。 例如,你有1 000 000
元素的std::vector
,每个元素在它的析构函数中抛出一个异常。 让我们假设std::vector
析构函数将其元素的析构函数中的所有异常收集到单个嵌套异常链中。 然后导致的异常甚至可能比原始的std::vector
容器更大。 这可能会导致性能问题,甚至在堆栈展开期间抛出std::bad_alloc
(甚至无法嵌套,因为没有足够的内存来执行此操作)或者在程序中的其他无关位置抛出std::bad_alloc
。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.