从析构函数中抛出异常的主要问题是,在调用析构函数的时候,另一个异常可能是"正在运行"(std::uncaught_exception() == true),因此在这种情况下不太明显该怎么做。用新的例外重写旧的例外是处理这种情况的可能方法之一。但决定在这种情况下必须调用std::terminate(或另一个std::terminate_handler)。
C++ 11通过EDCOX1×3类引入嵌套异常特征。此功能可用于解决上述问题。旧的(未捕获的)异常可以嵌套到新的异常中(反之亦然?)然后可以抛出嵌套的异常。但这个想法没有被使用。在C++ 11和C++ 14的这种情况下,仍然调用std::terminate。
所以问题是。是否考虑了嵌套异常的想法?有什么问题吗?在C++ 17中情况不会改变吗?
- 好吧,EDOCX1中析构函数的主要问题是对象不会被破坏。当对象不能被正确地破坏时,它是坏的。
- 另外,std::nested_exception要求嵌套的异常对象是可复制构造的。如果一个析构函数是由于正在处理的引发的异常而输入的,则不能保证引发的异常是可复制构造的。
- 当你在退绕过程中遇到异常情况时,你会怎么做?继续放卷?在你试图放松之前及时放松?
- @Zereges,对象和它的所有子对象也将被销毁。允许从析构函数引发异常,但在存在活动异常的情况下不允许。
- @Melak47,是的,继续放卷,但有两个活动异常。
- @泽雷格斯:经过研究,我发现安东是对的。第15.2节,p2清楚地指出,"任何对象的初始化或销毁被异常终止,将对其所有完全构造的子对象执行析构函数"。所以是的,如果你抛出一个析构函数,子对象仍然会被破坏。
- @sam varshavchik抛出不可复制构造的异常stackoverflow.com/a/12826028似乎是非法的:"当抛出的对象是类对象时,复制/移动构造函数和析构函数应该可以访问,即使删除了复制/移动操作(12.8)。"
- @安东·鲁尔:这与你所链接的帖子完全相反。只有在复制对象时才能访问复制构造函数。如果要移动对象,则必须可以访问移动构造函数。所以在标准中没有语言要求您抛出的类型是只移动的。但是,标准中有一种语言要求,如果您试图在其上使用current_exception,您抛出的类型是可复制的。
- @NicolBolas,我无法编译抛出不可复制构造对象的代码:coliru.stacked-crooked.com/a/82622616e148ad3d,尽管我没有使用current_exception。
- 归根结底,这是一个判断的要求,但我的观点是,为了让代码更容易阅读,应该有例外。在某些情况下,使用异常而不是C风格的错误处理将使您的代码对于大多数C++程序员来说更容易阅读,因为您将"工作"部分与"错误处理"部分分开,以及所有这些。开始使用析构函数抛出异常,使用所有的EDCOX1,4,EDCOX1,5的东西,这是一个点,其中至少有50%的C++程序员不再能够轻松地读取代码。国际海事组织
- @Anton_rh:对于可移动的类型,它工作得很好:coliru.stacked-crooked.com/a/12b55771fd6315c您忘记了删除复制构造函数也会删除移动构造函数,除非显式地默认它。
- @尼古拉斯感谢你的澄清,但我可能错误地否定了我的想法。我的意思是,从最佳实践的角度来看,插入析构函数可能看起来很奇怪(在这种情况下,一些概念可能更好地实现,析构函数中的异常不应该破坏该对象。此外,在析构函数中几乎不能捕获异常。所以从这个角度来看,插入析构函数对我来说有点奇怪。
- @Zereges:"而且,在析构函数中几乎不能捕获异常。"这是错误的;在析构函数中捕获异常与其他地方没有什么不同。我相信你的意思是你不能抓住析构函数抛出的异常。这也是错误的。自动变量的析构函数仍在声明它们的try块中调用。存在功能级try块。
- @尼古拉·博拉斯,是的,我知道。当我说它必须是可复制构造的,我认为它必须是可复制构造的或可移动构造的。是的,复制/移动可构造是不同的东西,但它们是相似的(它们为相同类型的现有对象创建了一个新对象),所以我刚才说必须是可复制可构造的。抱歉,如果我的评论有误导性。我引用的是"复制/移动构造函数和析构函数应该是可访问的"
当析构函数作为堆栈展开过程的一部分执行时(当对象不是作为堆栈展开过程的一部分创建时),您引用的问题会发生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来检测这个病例。它有极大的缺陷。
- …并且在C++ 17中被EDCOX1的0替换,但不用于这个用例——它有助于开发类似事务的操作。(+ 1)
- @理查德霍奇斯:这是用于这个用例的。具体来说,它可以用来检测对象是否由于堆栈展开而被破坏,或者对象是否在正在解包的对象的析构函数中被破坏。这就是允许它帮助处理事务的原因:允许析构函数理解调用它的原因。
- 关于"catch语句如何实际捕获不同类型的嵌套异常",这不是问题。我不喜欢标准库的嵌套异常,但它们并不是完全不可用的。尽管反对将它们用于抛出析构函数的论点有缺陷,但并不意味着抛出析构函数在一般情况下是有意义的。;-)
- 是的,但是否有其他现实世界中的激励问题领域,其中uncaught_exceptions比uncaught_exception更有用?我什么都不知道。
- @理查德霍奇斯:除了这些功能外,没有其他任何功能的实际应用程序。这就是为什么uncaught_exception被否决的原因:因为任何可能合法的使用都会被破坏。
- 所以问题是如何处理多个异常。如果你处理Y,你就错过了处理X。如果你处理X,你会错过处理Y。当然,您可以使用像if(e.has_nested())这样的结构,但是这样的代码很难维护。
- 我认为处理X比处理Y更重要,因为X是首要问题。因此,Y应该嵌套到X中,而不是相反。但这里又出现了另一个问题。如果抛出了新的异常Z,那么应该在哪里嵌套它?它应该作为第二个嵌套异常嵌套到X中(当前的nested_exception的实现不允许这样做),还是应该嵌套到Y中(因此它形成X->Y->Z)?而且目前执行的nested_exception只能将X嵌套到Y中,不能按顺序进行。
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。然后更新分隔符(以便第一个分隔符的分隔符不同)。做所有这些,作为初始化一个虚拟数组的一部分,然后我们将丢弃它。
- "没有用错误处理来污染程序逻辑",所以您的功能链中的所有try/catch块都不会"污染"任何东西?对不起,但我宁愿有"丰富的日志"而不是那些胡说八道。更好的方法是使用一个只存储调用堆栈的异常类型,这是通过平台特定的方法获得的。
- @nicolbolas函数try块的使用使rethrow远离程序逻辑的方式(实际上,它将是多行的)。调用堆栈非常好,但在错误日志文件中不太有用,您可能希望在其中存储额外的状态信息(例如this的内容)。catch块允许轻松地对所有数据进行排序。
- 这是一个非常好和有指导意义的例子,说明如何正确地使用异常!您是否有任何引用可以更详细地解释这种嵌套异常的方法,或者有一个开源代码的例子可以这样做?对于初学者来说,没有注释的代码有点难理解。
- @gpmueller添加了一些评论。我使用了一些"高级"技术来扩展模板,对此我深表歉意。遗憾的是,这种技术在库代码中使用不足,这是一种耻辱,因为它为需要它的用户提供了大量的细节。更多信息请访问:en.cppreference.com/w/cpp/error/throw_with_nested
- 一个问题是:std::rethrow_exception(std::current_exception());是否等同于throw;?
- @是的,我想你是对的。)
- 我能得到一些帮助来理解从void (expand{开始的路线吗?特别是,void (是用来做什么的,0, ((ss << sep << args), sep =",", 0)正在做什么,甚至在这种情况下...正在做什么。
- @Xenial为你更新了答案。
- 非常感谢你花时间来解释这个问题。我发现自己是在查看en.cppreference.com/w/cpp/language/parameter_pack之后发现的。顺便说一句,你最后一段可能想用"args"代替"params"。
- 这似乎意味着许多异常处理代码实际上并不处理异常。如果您想真正获得带注释的调用堆栈优势,几个抛出的函数很容易意味着向其他数十个函数添加代码。为了调试帮助,在项目代码中维护正确的代码是非常混乱的。我想肯定有地方可以去,我很高兴知道这一点。
- @Xenial的另一种选择是在抛出站点使用boost::stack_跟踪。你想走哪条路(如果有的话)将取决于你的用例和个人偏好。
嵌套异常只会添加最可能被忽略的有关发生的情况的信息,即:
引发了异常X,堆栈正在解除绑定,即调用本地对象的析构函数时出现异常&ldquo;in flight&rdquo;,而其中一个对象的析构函数又抛出异常Y。
通常这意味着清理失败。
然后,这不是一个可以通过向上报告并让更高级别的代码决定(例如,使用其他方法来实现其目标)来补救的失败,因为保存清理所需信息的对象已经与其信息一起被销毁,但没有进行清理。所以这很像一个失败的断言。进程状态可能非常不正常,违反了代码的假设。
原则上,抛出的析构函数是有用的,例如Andrei曾经提出过在块作用域退出时指示失败事务的想法。也就是说,在正常的代码执行中,没有被通知事务成功的本地对象可以从其析构函数中抛出。这只会成为一个问题,当它在堆栈展开时与C++规则冲突时,它需要检测是否可以抛出异常,这看起来是不可能的。不管怎样,析构函数只是用于它的自动调用,而不是在它的清理程序r中?乐。因此,可以得出结论,当前的C++规则假设了清理R?对于析构函数。
- 好,假设对象未能关闭文件或解锁互斥体。对象未被销毁,其所有信息都可用。但是,如何用更高级别的代码修复所描述的问题呢?这种方法的失效与析构函数的失效没有太大的区别。所有必要的信息都可以放在异常本身中。
- @安东:不确定你所说的"不销毁"是什么意思,但是在异常中放置延迟清理信息的想法本身就是在某些情况下处理事情的一种方法。这些信息也可以放在别处。最终,它需要某种机制来完成延迟的清理,或者让使用这些资源的其他代码知道。一般来说,它会很快变得混乱。这是一个核心语言无法解决的问题,只有当它变得更清楚需要什么支持时才能支持(这仍然不清楚,afaik)。
- 我不想把延迟的清理信息放到异常中。我的意思是,例如,如果fstream::close未能"清理"资源,您如何处理这个问题?即使close失败的fstream对象仍然可用(未销毁),也无法使用该对象解决问题(例如,第二次调用close时)。所以我认为从fstream::close或fstream::~fstream中抛出异常没有区别。在这两种情况下,你都无能为力。
- @安东:当清理失败时,你可以终止。在fstream::close的情况下,调用普通用户的代码可以这样做。但是在堆栈展开过程中析构函数失败的情况下,没有调用普通用户的代码,因此C++实现了。
通过从析构函数链接异常的堆栈展开过程中可能发生的问题是嵌套异常链可能太长。例如,您有1 000 000元素的std::vector,每个元素都在其析构函数中抛出异常。假设std::vector的析构函数将其元素的析构函数中的所有异常收集到嵌套异常的单链中。那么产生的异常可能比原始的std::vector容器还要大。这可能会导致性能问题,甚至在堆栈展开期间抛出std::bad_alloc(因为没有足够的内存来进行此操作,所以甚至不能嵌套)或在程序中其他不相关的地方抛出std::bad_alloc。
真正的问题是从析构函数中抛出是一个逻辑谬论。这就像定义运算符+()来执行乘法。析构函数不应用作运行任意代码的钩子。它们的目的是确定地释放资源。根据定义,这一定不会失败。其他任何东西都会破坏编写通用代码所需的假设。
- 根据定义?析构函数有时无法清理其资源(例如fstream::close)。在这种情况下,他们应该怎么做?终止整个程序可能不是所需的行为。把问题通知上面的代码,让它决定该怎么做,不是更好吗?
- 关闭失败时无需终止程序。fstream::close()不是析构函数,它是一个普通的成员函数。这是一件好事,因为它允许呼叫者说"闭嘴,照常报告错误",而不是"我不再需要你了,走开"。
- 关于析构函数,我也可以这么说:当未能关闭析构函数时,不需要终止程序(当前析构函数要么静默地忽略问题,要么终止整个程序,这两种行为都不好)。通过从析构函数中抛出异常来报告问题是可以的(这不会比从close中抛出更糟糕),如果没有问题等待异常的话。
- 只有当您选择让析构函数做的不仅仅是清理,从而打破范式,从而在异常不能用于报告这些错误的上下文中引入额外的潜在错误源时,这个问题才会存在。这意味着您现在必须想出其他方法来通知用户。这正是fstream::close()最初提供的。
- @Wilevers什么是"范式"?什么是"只是清理"?如果"just cleanup"返回错误,该怎么办?