c++编程习惯七(不要让析构函数抛出异常)

xiaoxiao2025-04-13  18

c++之中,异常处理是允许析构函数抛出异常的,但是,并不鼓励那样做,考虑一下这种情况:

class Student{ ... ~Student();//在这里吐出一个异常 }; void dosomething() { std::vector<Student> ss; ... }

当容器ss被销毁时,会销毁其内所含有的Student。假设ss内存了十个,在第一个Student析构时抛出异常,那这就很尴尬了,其他的九个就不能被销毁了,这时候就会出现不明确行为。所以c++不喜欢析构函数吐出异常。这是很容易理解的。

 

但是如果你的析构函数必须执行一个动作,而该动作可能会在失败时抛出异常,该怎么办?

假设使用一个class负责数据库连接:

class Dbconnection{ public: ... static Dbconnection create();//这个函数返回Dbconnection对象 void close(); }

为了确保客户不忘记调用close();一个合理的想法是创建一个用来管理资源的class,然后在析构函数中调用close:

class Dbconn{//用来管理Dbconnection对象 public: ... ~Dbconn()//确保数据库连接总是会被关闭 { db.close(); } private: Dbconnection db; }

这样就好啦,只要close正常调用,一切都很好,但是如果该调用导致异常,这时就很麻烦了,在析构函数出了异常。

 

我们有两个方法可以避免这一问题:

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.close(); } catch(...) { ... } } } private: Dbconnection db; bool closed; };

把调用close的责任从Dbconn析构函数手上给客户,有点甩锅的意思,其实并不是这样的,因为如果某个操作可能在失败时抛出异常,而又是存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。因为析构函数吐出异常就是危险,总会带来“过早结束程序”或“发生不明确行为”的风险。

 

总结:

1、析构函数绝对不要抛出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或者结束程序;

2、如果客户需求对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。

转载请注明原文地址: https://www.6miu.com/read-5028192.html

最新回复(0)