RAII与例外

我们在C ++中使用RAII的次数越多,发现自己的析构函数就越不重要。 现在,释放(最终确定,但是你想调用它)可能会失败,在这种情况下,exception确实是让楼上任何人知道我们的重新分配问题的唯一方法。 但是再一次,抛出析构函数是一个糟糕的主意,因为堆栈展开期间可能抛出exception。 std::uncaught_exception()可以让你知道什么时候发生了,但是除此之外,除了让你在终止之前logging消息之外,没有什么可以做的,除非你愿意让你的程序处于一个未定义的状态,有些东西被释放/定稿,有些则没有。

一种方法是有无抛出的析构函数。 但在很多情况下,这只是一个真正的错误。 例如,我们的析构函数可能会由于抛出一些exception而closures一些RAIIpipe理的数据库连接,而这些数据库连接可能无法closures。 这并不一定意味着我们可以在这个程序结束的时候继续。 另一方面,logging和追踪这些错误并不是每个案例的真正解决scheme, 否则我们就不需要例外了。 使用无抛析析构函数,我们也发现自己必须创build应该在破坏之前被调用的“reset()”函数,但是这只是打败了RAII的全部目的。

另一种方法就是让程序终止 ,因为这是你可以做的最可预测的事情。

有人build议链接exception,以便一次处理多个错误。 但是我真的从来没有真正看到过用C ++做的事情,我也不知道如何实现这样的事情。

所以它是RAII或例外。 不是吗? 我倾向于无耻的破坏者; 主要是因为它使事情变得简单(r)。 但我真的希望有一个更好的解决scheme,因为正如我所说的,我们使用RAII越多,我们就越发现自己使用的是不重要的东西。

附录

我添加链接到有趣的主题文章和我已经find的讨论:

  • 投掷破坏者
  • 关于SEH问题的 StackOverflow讨论
  • StackOverflow关于throwing-destructors的讨论(感谢,Martin York)
  • 乔尔在例外
  • SEH被认为是有害的
  • CLRexception处理也涉及exception链接
  • 草药Sutter对std :: uncaught_exception和为什么它没有你想象的那么有用
  • 有关参与者的历史讨论 (长!)
  • Stroustrup解释RAII
  • Andrei Alexandrescu的范围卫队

不能从析构函数中抛出一个exception。
如果exception已经传播,则应用程序将终止。

通过终止我的意思是立即停止。 堆放松停止。 没有更多的析构函数被调用。 所有坏东西。 看到这里的讨论。

从析构函数中抛出exception

我不遵循(不同意)你的逻辑,这导致析构函数变得更复杂。
随着智能指针的正确使用,这实际上使得析构函数更简单,因为现在一切都变成了自动的。 每个class级都会拼出自己的一小块拼图。 这里没有脑部手术或火箭科学。 RAII的另一大胜利。

至于std :: uncaught_exception()的可能性,我在Herb Sutters文章中指出了为什么它不起作用

从原来的问题来看:

现在,释放(最终确定,但是你想调用它)可能会失败,在这种情况下,exception真的是让楼上任何人知道我们的解除分配问题的唯一方法

清理资源失败表明:

  1. 程序员错误,在这种情况下,您应该logging失败,然后通知用户或终止应用程序,这取决于应用程序的情况。 例如,释放已经释放的分配。

  2. 分配器错误或devise缺陷。 请参阅文档。 机会是错误可能有帮助诊断程序员错误。 见上面的第1项。

  3. 否则不可恢复的不利条件可以继续。

例如,C ++免费商店有一个无故障运营商删除。 其他API(如Win32)提供错误代码,但只会因程序员错误或硬件故障而失败,错误指示堆损坏或双重空闲等情况。

至于不可恢复的不利条件,采取数据库连接。 如果由于连接断开而closures连接失败 – 很酷,就完成了。 不要扔! 一个断开连接(应该)导致一个closures的连接,所以没有必要做其他事情。 如果有的话,logging一个跟踪消息,以帮助诊断使用问题。 例:

 class DBCon{ public: DBCon() { handle = fooOpenDBConnection(); } ~DBCon() { int err = fooCloseDBConnection(); if(err){ if(err == E_fooConnectionDropped){ // do nothing. must have timed out } else if(fooIsCriticalError(err)){ // critical errors aren't recoverable. log, save // restart information, and die std::clog << "critical DB error: " << err << "\n"; save_recovery_information(); std::terminate(); } else { // log, in case we need to gather this info in the future, // but continue normally. std::clog << "non-critical DB error: " << err << "\n"; } } // done! } }; 

这些条件都没有理由尝试第二种放松。 程序可以正常继续(包括exception放松,如果展开正在进行),或者它在这里和现在死亡。

编辑-添加

如果你真的希望能够保持某种与那些不能closures的数据库连接的链接 – 也许由于间歇性的条件而不能closures,而且你想稍后再试 – 那么你总是可以推迟清理:

 vector<DBHandle> to_be_closed_later; // startup reserves space DBCon::~DBCon(){ int err = fooCloseDBConnection(); if(err){ .. else if( fooIsRetryableError(err) ){ try{ to_be_closed.push_back(handle); } catch (const bad_alloc&){ std::clog << "could not close connection, err " << err << "\n" } } } } 

非常不漂亮,但它可能会为你完成这项工作。

当我向他解释例外/ RAII概念时,它提醒了我一个同事的问题:“嘿,如果closures计算机,我可以抛出什么exception?

无论如何,我同意马丁·约克的答案RAII与例外

什么是exception和析构函数?

很多C ++特性依赖于非抛出析构函数。

实际上,RAII的整个概念及其与代码分支(返回,抛出等)的合作基于事实上的释放不会失败。 以同样的方式,当你想为你的对象提供很高的exception保证时,一些函数不应该失败(比如std :: swap)。

这并不意味着你不能通过析构函数抛出exception。 只是语言甚至不会试图支持这种行为。

如果授权会发生什么?

只是为了好玩,我试图想象它…

如果你的析构函数没有释放你的资源,你会怎么做? 你的对象可能是一半被破坏的,你会从“外部”捕获那些信息做什么? 再试一次? (如果是,那么为什么不从析构函数中再次尝试?)

也就是说,如果你可以访问你的半破坏对象:如果你的对象在栈上(这是RAII工作的基本方式)是什么? 如何访问其范围之外的对象?

发送exception内的资源?

你唯一的希望就是发送exception内的资源“处理”,并希望在catch中的代码,以及…再次尝试释放它(见上文)?

现在想象一下有趣的事情:

  void doSomething() { try { MyResource A, B, C, D, E ; // do something with A, B, C, D and E // Now we quit the scope... // destruction of E, then D, then C, then B and then A } catch(const MyResourceException & e) { // Do something with the exception... } } 

现在,让我们想象一下,由于某种原因,D的析构函数无法解除分配资源。 你编码发送一个exception,这将被捕获。 一切顺利:你可以按照你想要的方式来处理失败(你将如何以一种build设性的方式避开我,但是现在不是这个问题)。

但…

在MULTIPLEexception中发送MULTIPLE资源?

现在,如果〜D可以失败,那么〜C也可以。 以及〜B和〜A。

用这个简单的例子,你有4个析构函数在“同一时刻”失败(退出范围)。 你所需要的不是一个exception的捕获,而是一个捕获exception数组(我们希望为此生成的代码不会抛出)。

  catch(const std::vector<MyResourceException> & e) { // Do something with the vector of exceptions... // Let's hope if was not caused by an out-of-memory problem } 

让我们重新开始( 我喜欢这个音乐…… ):抛出的每个exception都是不同的( 因为原因是不同的:请记住,在C ++中,exception不需要从std :: exception中派生出来 )。 现在,你需要同时处理四个例外。 你怎么能写出处理这四个exceptiontypes的catch子句,以及它们被抛出的顺序?

那么如果你有多个相同types的exception,由多个失败的释放引发? 而如果在分配数组的exception数组的内存时,你的程序将耗尽内存,并呃抛出内存exception?

你确定你想花时间在这类问题上,而不是花时间去研究为什么释放失败或者如何以另一种方式对它做出反应?

实际上,C ++devise者并没有看到一个可行的解决scheme,只是减less了他们的损失。

问题不是RAII vs例外…

不,问题是,有时候,事情可能会失败,以至于什么都不能做。

只要满足一些条件,RAII就可以很好地适用于Exceptions。 其中: 析构函数不会抛出你所看到的作为对立只是一个组合了两个“名字”的例子:例外 RAII

在析构函数中发生问题的时候,我们必须接受失败,并打捞出可以挽救的东西 :“数据库连接不能被解除分配?对不起,让我们至less避免这个内存泄漏并closures这个文件。

虽然exception模式(应该是)是C ++中的主要error handling,但并不是唯一的。 当C ++exception不是解决scheme时,您应该使用其他错误/日志机制来处理exception(双关意图)的情况。

因为你刚刚用语言碰到了一面墙,所以没有其他的语言可以正确的穿过我的房子(C#尝试是值得的,而Java的那个仍然是一个让我伤心的笑话) …我甚至不会谈论脚本语言在同样的问题上会以同样的沉默方式失败)。

但是最终,无论您要写多less代码,用户都不会受到closures计算机的保护

你可以做的最好的,你已经写了。 我自己的喜好与抛出finalize方法,非抛出析构函数清理资源没有手动确定,和日志/ messagebox(如果可能)提醒有关在析构函数的失败。

也许你没有做出正确的决斗。 应该是“ 试图释放资源与绝对不希望被释放的资源,即使在受到破坏威胁的情况下

🙂

你正在看两件事情:

  1. RAII,确保在退出范围时清理资源。
  2. 完成一项操作并查明是否成功。

RAII承诺它将完成操作(空闲内存,closures试图刷新它的文件,结束一个试图提交它的事务)。 但是因为它是自动发生的,程序员不需要做任何事情,它并不告诉程序员这些“尝试”的操作是否成功。

例外是报告失败的一种方法,但正如你所说,C ++语言的限制意味着它们不适合从析构函数中做到这一点[*]。 返回值是另外一种方式,但更明显的是析构函数也不能使用它们。

所以,如果你想知道你的数据是否写入磁盘,那么你不能使用RAII。 它不会“破坏RAII的全部目的”,因为RAII仍然会尝试写入它,并且仍然会释放与文件句柄(数据库事务,无论)有关的资源。 它确实限制了RAII能做什么 – 它不会告诉你数据是否被写入,所以你需要一个close()函数来返回值和/或抛出exception。

[*]这是相当自然的限制,也出现在其他语言。 如果你认为RAI​​I的析构函数应该抛出exception来说“出了什么问题!”,那么当已经有一个exception情况出现的时候,就会发生一些事情,就是“在别的之前出现了什么问题! 我知道使用例外的语言不允许同时在飞行中出现两个例外 – 语言和语法根本不允许。 如果RAII要做你想做的事情,那么exception本身就需要重新定义,这样一个线程就有一个以上的东西出错了,而有两个exception向外传播,两个处理器被调用,一个来处理每个。

其他语言允许第二个exception模糊第一个exception,例如,如果finally块抛出Java。 C ++几乎说第二个必须被压制,否则terminate被调用(在某种意义上压制两者)。 在这两种情况下都没有通知两个故障的更高的堆叠水平。 有点不幸的是,在C ++中,你不能可靠地判断一个uncaught_exceptionexception是多less( uncaught_exception并没有告诉你,它告诉你一些不同的东西),所以你甚至不能抛出这种情况飞行中还没有例外。 但即使你能做到这一点,如果再多一个,你还是会被塞进来的。

我要问的一件事是,忽略终止等问题,如果程序无法closures其数据库连接(由于正常销毁或exception破坏),您认为适当的响应是什么。

你似乎排除了“只是伐木”而不愿终止,那么你认为最好的办法是什么?

我想,如果我们对这个问题有一个回答,那么我们就可以更好地了解如何进行。

没有什么策略对我来说似乎特别明显 除了别的之外,我真的不知道closures数据库连接是什么意思。 如果close()抛出什么状态的连接? 它是封闭的,还是开放的,还是不确定的? 如果不确定,程序是否有办法恢复到已知的状态?

析构失败意味着无法撤销创build对象; 将程序返回到已知(安全)状态的唯一方法是拆除整个过程并重新开始。

你销毁可能失败的原因是什么? 为什么不在真正的破坏之前去处理那些呢?

例如,closures数据库连接可能是因为:

  • 交易正在进行中。 (检查std :: uncaught_exception() – 如果为true,则回滚,否则提交 – 除非在实际closures连接之前有另外的策略,否则这些操作是最可能需要的操作。
  • 连接被丢弃。 (检测并忽略,服务器将自动回滚。)
  • 其他数据库错误。 (logging下来,以便我们可以在将来进行调查,并可能进行适当的处​​理,这可能是检测和忽略,同时尝试回滚并再次断开连接,忽略所有错误。

如果我正确理解RAII(我可能不会),那么整个问题就是它的范围。 所以它不像你想交易持续时间比对象更长。 那么,我认为你应该尽可能地确保closures。 RAII并没有使这种独特 – 即使没有任何对象(比如在C中),你仍然会试图捕捉所有的错误条件并尽可能地处理它们(有时忽略它们)。 所有RAII所做的是强制你把所有的代码放在一个地方,不pipe有多less个函数正在使用这个资源types。

您可以通过检查是否存在当前正在执行的exception(例如,我们处于执行堆栈展开的throw和catch块之间,可能是复制exception对象等)

 bool std::uncaught_exception() 

如果它返回true,则抛出此时将终止程序,如果没有,则抛出(或至less与以前一样安全)是安全的。 这在ISO 14882(C ++标准)的第15.2和15.5.3节中讨论。

这并不能回答在清理exception时遇到错误时该怎么做的问题,但实际上没有任何好的答案。 但是,如果你在后一种情况下等待做一些不同的事情(如logging和忽略),而不是简单的恐慌,它可以让你区分正常退出和exception退出。