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