c++並不禁止解構函式拋出異常,但它不鼓勵這樣做:
class Widget{
public:
…
~Widget(); //假設可能拋出異常
};
void doSomething()
{
std::vector<Widget> v;
…
}
當vector v被銷毀,它有責任銷毀其內含的所有Widgets。假設在析構第一個元素期間,有個異常被拋出,其他還是應該被銷毀,因此v應該調用它們各個解構函式。但假設第二個Widget解構函式由拋出異常。現在,有兩個同時作用的異常,這對c++來說太多了,兩個異常同時存在的情況下,程式若不是結束執行,就是導致不明確的行為。即使並不是使用容器或者arrays, 程式也可能過早結束或出現不明確行為。c++不喜歡解構函式拋出異常。
假如你的解構函式必須執行一個動作,而該動作可能會在失敗時拋出異常,該怎麼辦?如:有個負責資料庫連接的class:
class DBConnection{
public:
…
static DBConnection create();
void close(); //關閉聯機, 失敗則拋出異常。
};
為了客戶不忘記在DBConnection對象上調用close(),一個合理的想法是建立一個用來管理DBConnection資源的class,並在解構函式中調用close():
class DBConn{
public:
…
~DBConn()
{
db.close();
}
private:
DBConnection db;
};
這使客戶可以寫這樣的代碼:
{
DBConn dbc(DBConnection::create());
…
} // 區塊結束後,DBConn對象被銷毀,因而自動為DBConnection對象調用close。
如果調用close成功,一切都美好,但如果該調用導致異常,DBConn解構函式會傳播該異常,也就是允許它離開這個解構函式。那會造成問題,因為那就是拋出了難以駕馭的麻煩。
有兩個方法可以避免這個問題,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是否已經關閉,並在未關閉的情況下由其解構函式關閉之。這可以防止遺失資料庫連接。然而如果DBConn解構函式調用close失敗,我們又將退回到“強迫結束程式”或“吞下異常”的老路。
class DBConn{
public:
…
void close()
{
db.close();
closed = true;
}
~DBConn()
{
if (!closed){
try{ db.close();}
catch(…){製作運轉記錄,記下對close調用的失敗;
…
}
}
}
private:
DBConnection db;
bool closed;
};
如果某個操作可能在失敗時拋出異常,而又存在某種需要必須處理該異常,那麼這個異常必須來自解構函式以外的某個函數。因為解構函式拋出異常就是危險,總會帶來“程式過早結束”或“發生不明確行為”的風險。由客戶自己調用close並不會對他們帶來負擔, 而是給他們一個處理錯誤的機會,否則他們沒機會響應。如果他們不認為這個機會有用(或許他們堅信不會有錯誤發生),可以忽略它,依賴DBConn的解構函式來調用close。如果有錯誤發生,close的確拋出異常--並且DBConn吞下該異常或結束程式,客戶沒有立場抱怨,畢竟他們曾有機會第一手處理問題,但他們選擇了放棄。
解構函式絕對不能拋出異常,如果一個解構函式調用的函數可能拋出異常,解構函式應該能捕捉任何異常,然後吞下它(不傳播)
或結束程式。
如果客戶需要對某個操作函數運行期間拋出的異常作出反應,那麼class應該提供一個普通函數(而非在解構函式中)執行該操作。