Effective C++ 条款8 析构函数的异常管理
条款08 : 别让异常逃离析构函数
日常编程中,常见异常通常由以下原因引发:
- 资源管理不当(如内存泄漏, 文件操作, 连接或断开连接失败)。
- 边界和合法性检查不足(如数组越界、除以零)。
- 并发编程问题(如死锁、数据竞争)。
- 类型不匹配或错误的操作。
本条款主要讨论的是析构函数的异常管理, 为什么会不希望异常逃离析构函数, 因为析构函数是一个必须执行且有可能失败的函数, 因为内存泄漏, 文件操作, 连接或断开连接失败等资源管理行为的错误都是很难避免的, 而且由于析构函数是递归式调用并且可能一次性销毁大量结构, 同时产生大量异常的概率就提高了, 书中指出, 如果同时存在多个异常, 程序不是结束执行就是导致不明确行为, 因此对于析构函数的异常管理在所难免.
书中举出了一个数据库连接的例子 :
1 |
|
我们经常会设计一个资源管理类来控制数据库的连接 :
1 |
|
于是我们就可以写出以下代码 :
1 |
|
有了上面例子的基础, 我们来思考一下如何避免异常抛出吧.
站在DBConn析构函数
的角度, 书中给出了两个一般的方法 :
如果close()抛出异常就利用abort()结束程序.
1
2
3
4
5
6
7
8DBConn::~DBConn()
{
try { db.close(); }
catch (...) {
// 记录日志, 记录对close的调用失败
std::abort(); // 直接结束程序
}
}直接吞下该异常.
1
2
3
4
5
6
7
8DBConn::~DBConn()
{
try { db.close(); }
catch (...) {
// 记录日志, 记录对close的调用失败
// 不做处理, 直接吞下
}
}
这两个方法其实都是保底方法, 一个是草率结束进程, 一个是吞掉异常防止扩散, 但其实DBConn析构函数
也就能做这么多了.
现在的问题是没有办法对”导致close抛出异常”的情况做出反应, 问题核心在close()
函数被DBConn析构函数
掌握, 只能其自己管理, 上一层无法干预. 那么由此我们可以将close()函数的使用权上交, 也就是让上一层用户有权决定处理close()
报错的方式.
较佳策略是重新设计DBConn接口, 使客户有机会对可能出现的问题作出反应.
具体做法如下 :
DBConn
自己也提供一个close接口, 内部封装上DBConnection
的close()
.DBConn
自己对DBConnection
的close()
是否已经触发进行追踪管理, 如果到最后客户都没有自行close成功, 由DBConn析构函数
自行完成close的任务.
我们来看书中给出的代码 :
1 |
|
于是客户便可做出如下操作 :
1 |
|
如果只考虑数据库断开连接的场景, 我们可行的具体操作可以是重试关闭或执行数据库回滚, 代码如下 :
1 |
|
我们再来捋一下流程, 先是客户需要考虑何时调用close()
并写出应对异常的代码, 实际运行时如果没有异常就万事大吉, 有异常就触发客户的处理逻辑, 最后析构函数检查客户是否真的成功调用了close()
, 如果还是没有调用, 就自己调用, 自己调用如果还出错, 就直接结束程序或吞掉异常.
至此我们将调用close()
的责任从DBConn析构函数
的手上移交到了使用DBConn对象
的客户手上, 可以更好地避免异常逃离析构函数. 有人可能这样会加大客户的操作负担, 但是根据我们先前的分析, 只有客户才能有办法对”导致close抛出异常”的情况做出反应, 这样做是给客户提供一个根据实际情况回避异常的机会, 至于是否需要就看客户自己了.
请记住 :
- 析构函数绝对不要吐出异常. 如果分析出一个析构函数有抛出异常的风险, 应当先把异常捕捉下来, 看是否结束程序或吞下异常.
- 给客户提供自己处理异常的机会, 让客户可以根据实际情况对异常做出反应.
by 天目中云