我们在C++中使用RAII的次数越多,我们就越发现自己的析构函数会进行非平凡的释放.现在,解除分配(终结,但是你想要调用它)可能会失败,在这种情况下,异常实际上是让楼上的任何人知道我们的释放问题的唯一方法.但是再说一次,抛出析构函数是一个坏主意,因为在堆栈展开期间可能会抛出异常.std::uncaught_exception()
让你知道什么时候发生,但不是更多,所以除了让你在终止之前记录一条消息之外你没有太多可以做的,除非你愿意让你的程序处于未定义的状态,其中一些东西被解除分配/最终化而一些不是.
一种方法是使用无抛出析构函数.但在许多情况下,这只是隐藏了一个真正的错误.例如,我们的析构函数可能会因为抛出某些异常而关闭一些RAII管理的数据库连接,并且这些数据库连接可能无法关闭.这并不一定意味着我们可以在此时终止程序.另一方面,记录和跟踪这些错误并不是每个案例的真正解决方案; 否则我们就不需要开始例外了.使用无抛出析构函数,我们还发现自己必须创建应该在销毁之前调用的"reset()"函数 - 但这只会破坏RAII的整个目的.
另一种方法是让程序终止,因为这是你可以做的最可预测的事情.
有些人建议链接异常,以便一次可以处理多个错误.但老实说,我从来没有真正看到用C++完成的工作,我也不知道如何实现这样的东西.
所以它是RAII或例外.不是吗?我倾向于无抛出的破坏者; 主要是因为它保持简单(r).但我真的希望有一个更好的解决方案,因为,正如我所说,我们使用RAII的次数越多,我们发现自己越多地使用执行非平凡事情的dtors.
附录
我正在添加链接到我发现的有趣的主题文章和讨论:
投掷析构函数
StackOverflow讨论了SEH的问题
关于throw-destructors的 StackOverflow讨论(谢谢,Martin York)
Joel on Exceptions
SEH被认为是有害的
CLR异常处理也触及异常链接
关于std :: uncaught_exception的Herb Sutter以及为什么它没有你想象的那么有用
有趣的参与者对此事的历史性讨论(很长!)
Stroustrup解释RAII
Andrei Alexandrescu的范围守卫
Martin York.. 18
你不应该从析构函数中抛出异常.
注意:已更新以更新标准中的更改:
在C++ 03中
如果异常已经传播,则应用程序将终止.
在C++ 11中
如果析构函数是noexcept
(默认值),那么应用程序将终止.
以下是基于C++ 11
如果异常转义noexcept
函数,则在堆栈甚至解除时,它是实现定义的.
以下是基于C++ 03
终止,我的意思是立即停止.堆栈展开停止.不再需要析构函数.所有不好的东西.请参阅此处的讨论.
从析构函数中抛出异常
我不遵循(如不同意)你的逻辑,这会导致析构函数变得更复杂.
通过正确使用智能指针,这实际上使析构函数更简单,因为现在所有内容都变为自动化.每个班级都会将自己的小部分拼凑出来.这里没有脑部手术或火箭科学.RAII的另一大胜利.
至于std :: uncaught_exception()的可能性,我指出Herb Sutters关于它为什么不起作用的文章
你不应该从析构函数中抛出异常.
注意:已更新以更新标准中的更改:
在C++ 03中
如果异常已经传播,则应用程序将终止.
在C++ 11中
如果析构函数是noexcept
(默认值),那么应用程序将终止.
以下是基于C++ 11
如果异常转义noexcept
函数,则在堆栈甚至解除时,它是实现定义的.
以下是基于C++ 03
终止,我的意思是立即停止.堆栈展开停止.不再需要析构函数.所有不好的东西.请参阅此处的讨论.
从析构函数中抛出异常
我不遵循(如不同意)你的逻辑,这会导致析构函数变得更复杂.
通过正确使用智能指针,这实际上使析构函数更简单,因为现在所有内容都变为自动化.每个班级都会将自己的小部分拼凑出来.这里没有脑部手术或火箭科学.RAII的另一大胜利.
至于std :: uncaught_exception()的可能性,我指出Herb Sutters关于它为什么不起作用的文章
从原来的问题:
现在,解除分配(终结,但是你想要调用它)可能会失败,在这种情况下异常实际上是让楼上的任何人知道我们的释放问题的唯一方法
无法清理资源或者表明:
程序员错误,在这种情况下,您应该记录失败,然后通知用户或终止应用程序,具体取决于应用程序方案.例如,释放已经释放的分配.
分配器错误或设计缺陷.请参阅文档.错误很可能有助于诊断程序员错误.见上文第1项.
否则不可恢复的不利条件可以继续.
例如,C++免费存储具有无失败运算符删除.其他API(如Win32)提供错误代码,但只会因程序员错误或硬件故障而失败,错误指示堆损坏或双重释放等情况.
至于不可恢复的不利条件,请采用DB连接.如果关闭连接失败,因为连接被删除 - 很酷,你就完成了.不要扔!断开的连接(应该)导致关闭连接,因此不需要做任何其他事情.如果有,请记录跟踪消息以帮助诊断使用问题.例:
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! } };
这些条件都没有理由尝试第二种放松.程序可以正常继续(包括异常展开,如果正在进行展开),或者它在此处和现在都会消失.
编辑 - 添加
如果你真的希望能够保持与那些无法关闭的数据库连接的某种链接 - 也许它们由于间歇性条件而无法关闭,而你想稍后重试 - 那么你总是可以推迟清理:
vectorto_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" } } } }
非常不漂亮,但它可能会为你完成工作.