条款08:别让异常逃离析构函数
Prevent exceptions from leaving destructuors
多个异常的抛出
在C++中,并不禁止析构函数吐出异常,但是它并不鼓励这样做。举个例子:
class Widget {
public:
...
~Widget() { ... } //假设这段代码会吐出一个异常
};
void doSomething()
{
std::vector<Widget> v;
...
} //v在这里被自动销毁
在上面的代码中,当vector v被销毁,它将会负责销毁其内的所有Widgets。
假设v内含有是个Widgets,而在析构第一个元素期间,有一个异常被抛出。而其他的九个Widget还是应该被销毁,否则他们保存的资源将会发生泄漏,因此v应该调用它们的析构函数。
但是,在第二个析构,又有一个异常被抛出!这对于C++来说,异常太多了。
在这个例子里,容器或者array并不是这种问题的必要条件。只要析构函数吐出异常,即使使用的并非是容器或者array,程序也可能出现过早结束或者出现不明确的行为。
因此,C++并不希望析构函数吐出异常。
传播异常
但是,假设析构函数必须执行一个操作,而该动作却可能会在失败时抛出异常,这时则如何去做?
举个例子,设计一个class负责数据库的连接:
class DBConn { //该class负责管理DBConnection对象
public:
...
~DBConn() //确保数据库连接总是会被关闭
{
db.close();
}
private:
DBConnection db;
};
在使用的过程中:
{
//开启一个区块(block),建立DBConnection对象
//并交给DBConn对象以便管理。
//通过DBConn接口使用DBConnection对象。
//在区块结束点,DBConn对象会被销毁,
//因而自动为DBConnection对象调用close
DBConn dbc(DBConnection""create());
...
}
在上面的例子中,如果调用close成功,则程序并不会出现什么问题。但是,如果该调用导致了异常,DBConn析构函数将会传播该异常——允许它离开这个析构函数。
两个解决方法:
1、 如果close抛出异常,就结束程序。通常通过调用abort完成。
DBConn::~DBConn()
{
try { db.close(); }
catch ( ... ) {
定义一个运转记录,记下对close的调用失败
std::abort();
}
}
利用这种调用abort函数的方法,可以阻止异常从析构函数中传播出去,进而避免了“不明确行为”的错误。
2、吞下因为调用close而发生的异常。
DBConn::~DBConn()
{
try { db.close(); }
catch ( ... ) {
定义一个运转记录,记下对close的调用失败
}
}
一般而言,将异常吞掉是一个坏主意,因为它压制了“某些动作失败”的重要信息。
但是,有时候吞下异常也比“不明确行为所带来的风险”要好。
上述的两种方法,并不是最佳的解决方案,因为二者都无法对“导致close抛出异常”的情况作出最好的反应。
给用户提供一个处理异常的机会
最佳的解决办法则是:
- 重新设计DBConn接口,使得用户有机会对可能出现的问题作出反应。
例如,DBConn自己可以提供一个close函数,因而赋予了用户一个机会得以处理“因该操作而发生的异常”。同时,DBConn也可以追踪所管理的DBConnection是否已经被关闭,并在答案为否的情况下调用析构函数将其关闭。
然而,如果DBConnection析构函数调用close失败,则又会回到“强迫结束程序”或“吞下异常”的老路:
class DBConn {
public:
...
void close() //供用户使用的新函数
{
db.close();
closed = true;
}
~DBConn()
{
if(!closed) {
try { db.closed(); } //关闭连接(如果用户不这么做的)
catch ( ... ) { //如果关闭动作失败,则记录下来并结束程序或吞下异常
制作运转记录,记下对close的调用失败;
...
}
}
}
private:
DBConnection db;
bool closed;
};
在上面的例子可以得到:
- 如果某个操作可能在失败时抛出异常,而又必须要去处理该异常,则这个异常必须来自析构函数以外的某个函数。
因为析构函数吐出异常,会带来“过早结束程序”或“发生不明确行为”的风险。
最后: