当我向同事解释异常/RAII概念时,他问了我一个问题:“嘿,如果电脑关闭了,我可以抛出什么异常?”
无论如何,我同意Martin York的回答RAII vs. exceptions
异常和析构函数有什么关系?
很多C++特性依赖于非抛出的析构函数。
实际上,整个RAII概念及其与代码分支(返回、抛出等)的协作都基于释放不会失败这一事实。同样,当您希望为对象提供高级别的异常保证时,某些函数不应该失败(例如std::swap)。
并不是说您不能通过析构函数抛出异常。只是语言甚至不会尝试支持这种行为。
如果允许这样做会发生什么?
只是为了好玩,我试着想象一下...
如果您的析构函数无法释放资源,您将怎么办?您的对象可能已经被部分析构,从“外部”catch中获取到这些信息后,您将怎么处理?重试吗?(如果是的话,那为什么不在析构函数内部重试呢?...)
也就是说,如果您无论如何都可以访问您的半析构对象:如果您的对象在堆栈上(这是RAII的基本工作方式),那么您如何访问其作用域之外的对象?
将资源发送到异常中?
您唯一的希望就是将资源的“句柄”发送到异常中,并希望catch中的代码能够再次尝试释放它(请参见上文)?
现在,想象一些有趣的事情:
void doSomething()
{
try
{
MyResource A, B, C, D, E ;
}
catch(const MyResourceException & e)
{
}
}
现在,让我们想象一下由于某种原因D的析构函数未能释放资源。你编码发送了一个异常,这将被catch捕获。一切顺利:您可以以您想要的方式处理失败(如何以建设性的方式处理仍然使我困惑,但现在不是问题)。
但是......
如何在MULTIPLE异常中发送MULTIPLE资源?
现在,如果~D失败了,那么~C也会失败。 ~B和~A也是如此。
通过这个简单的例子,您有4个析构函数在“同一时刻”失败(退出作用域)。你所需要的不是含有一个异常的catch,而是一个带有异常数组的catch(希望为此生成的代码不会...抛出异常)。
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
}
让我们开始吧(我喜欢这首歌):每个抛出的异常都是不同的(因为原因不同:请记住,在C++中,异常不需要派生自std::exception)。现在,您需要同时处理四个异常。您如何编写catch子句来处理四个异常类型,并按它们被抛出的顺序进行处理?
如果有多个相同类型的异常由多个失败的释放引发怎么办?如果在分配异常数组的数组内存时,您的程序耗尽了内存并且...抛出了内存不足异常呢?
您确定要花时间解决这种问题而不是花时间弄清楚为什么释放失败或以其他方式对其做出反应吗?
显然,C++设计师没有看到可行的解决方案,只是在那里减少损失。
问题不在于RAII与异常之间的关系...
不,问题在于有时候事情会失败得无法挽回。
RAII与异常结合使用效果很好,只要满足一些条件。其中之一是:析构函数不会抛出异常。你所看到的对立只是一个单一模式的极端情况,结合了两个“名字”:异常和RAII
在析构函数中遇到问题时,我们必须接受失败,并挽救可以挽救的东西:“DB连接无法被释放?对不起。让我们至少避免这个内存泄漏并关闭此文件。”
虽然异常模式(应该)是C++中的主要错误处理,但它不是唯一的错误处理方式。当C++异常不是解决方案时,您应该使用其他错误/日志机制来处理异常情况。
因为您在语言中遇到了障碍,没有其他我所知道或听说过的语言能够正确地通过这个问题而不会把房子拆掉(C#尝试是值得的,而Java的尝试仍然是伤害我的笑话...我甚至不想谈论脚本语言,他们将以同样的沉默方式失败)。
但最终,无论您写多少代码,用户关闭电脑时都无法保护您。
您可以做的最好的事情,已经写出来了。我的个人偏好是使用抛出finalize方法、非抛出析构函数清理未手动完成的资源以及日志/消息框(如果可能)来警报有关析构函数失败的信息。
也许您没有准备好正确的决斗。与其是“RAII vs. Exception”,不如是“尝试释放资源 vs. 这些资源绝对不想被释放,即使受到破坏的威胁”。