1. 程式人生 > >c++程式設計習慣七(不要讓解構函式丟擲異常)

c++程式設計習慣七(不要讓解構函式丟擲異常)

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應該提供一個普通函式(而非在解構函式中)執行該操作。