C++并不禁止析构函数抛出异常,但它不鼓励你这样做。考虑以下代码:
class Widget {
public:
...
~Widget() {...} // 可能抛出异常
};
void doSomething() {
std::vector<Widget> v;
...
} // v被销毁
假设v有十个Widget,而在析构第一个元素期间,有个异常被抛出。假设在调用剩下元素的析构函数又抛出了异常。现在有多个同时作用的异常,程序可能会终止也可能导致未定义行为。C++不喜欢析构函数抛出异常。
如果你的析构函数必须执行一个操作,而该操作可能会在失败时抛出异常,该怎么办?举个例子,假设你使用一个class负责数据库连接:
class DBConnection {
public:
...
static DBConnection create();
void close(); // 关闭联机,失败则抛出异常
};
为确保客户不忘记在DBConnection对象上调用close(),一个合理的想法是创建一个用来管理DBConnection资源的class,并在其析构函数中调用close。这只需要考虑它们的析构函数就够了:
class DBConn {
public:
...
~DBConn() {
db.close();
}
private:
DBConnection db;
};
这便允许客户写出这样的代码:
{ // Scope for dbConnection
DBConn dbc(DBConnection::create()); // 建立DBConnection对象
// 并交给DBConn对象以便管理
... // 使用DBConnection对象
} // DBConn对象被销毁,自动调用close
只要close成功,一切都美好。但如果该调用导致异常,DBConn析构函数会允许它离开析构函数。那会造成问题。
有两个办法可以避免这一问题。DBConn的析构函数可以:
-
如果close抛出异常就结束程序。通常通过调用abort完成:
DBConn::~DBConn() { try { db.close(); } catch (...) { std::abort(); } }
如果析构函数抛出异常,强迫结束程序是个合理选项。它可以阻止异常传播出去。
-
捕获因调用close而发生的异常但不处理
DBConn::~DBConn() { try { db.close(); } catch (...) {} }
一般而言,将异常捕获不处理是个坏主意,因为它压制了某些操作失败的重要信息。为了能让这成为一个可行方案,程序必须能够继续可靠地执行,即使在遇到异常后。
一个更好的策略是重新设计DBConn接口,使客户有机会对可能出现的异常做出反应。
class DBConn {
public:
void close() {
db.close();
closed = true;
}
~DBConn() {
if (!closed) {
try { db.close(); }
catch (...) {
// 结束程序或吞掉异常
}
}
}
private:
DBConnection db;
bool closed;
};
把调用close的责任从DBConn析构函数手上移到DBConn客户手上(但DBConn析构函数内仍有一个双保险调用)。如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。由客户自己调用close并不会对他们带来负担,而是给他们一个处理错误的机会。
请记住
- 析构函数绝对不要抛出异常。如果一个析构函数调用的函数可能抛出异常,析构函数应该捕获任何异常,然后吞下他们或结束程序。
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非析构函数中)执行该操作。