Prevent exceptions from leaving destructors
本條款主要講述當我們寫代碼的時候,如果出現了異常,讓我們如何對異常處理,如何更加安全的處理異常。讓我們的代碼看起來更加完美。
1、問題來源
案例1
由於C++並不禁止析構函數吐出異常,但它並不鼓勵你這麼做,所以,當我們不小心寫出了問題代碼的時候。考慮下面的代碼:
c++並不禁止析構函數吐出異常,但它並不鼓勵你這麼做。考慮下面的代碼:
class Widget
{
public:
~Widget(){...} //假設這個可能吐出一個異常
};
void dosomething()
{
vector<Widget> v;
} //v在這裡被自動銷毀
當vector被銷毀,它有責任銷毀其內含的所有Widgets。假設v內含十個Widgets,而在析構第一個元素期間,有個異常被拋出。其他九個widgets還是應該被銷毀,因此v應該調用它們各個析構函數。但假設在那些調用期間,第二個widget析構函數又拋出異常。現在有兩個同時作用的異常,這對c++而言太多了。
在兩個異常同時存在的情況下,程序若不是結束執行就是導致不明確行為。本例中會導致不明確的行為。使用標准程序庫的任何其他容器或TR1的任何容器或甚至array,也會出現相同情況。容器或array並非遇上麻煩的必要條件,只要析構函數吐出異常,即使並非使用容器或arrays,程序也可能過早結束或出現不明確行為。使得,c++不喜歡析構函數吐出異常!
案例2
這很容易理解,但如果你的析構函數必須執行一個動作,而該動作可能會在失敗時拋出異常,該怎麼辦?舉個例子,假設你使用一個class負責數據庫連接:
class DBConnection {
public:
...
static DBConnection create(); //這個函數返回DBConnection對象;為求簡化暫略參數。
void close(); //關閉聯機;失敗則拋出異常。
};
2、解決方案
為確保客戶不忘記在DBConnection對象身上調用close(),一個合理的想法是創建一個用來管理DBConection資源的class,並在其析構函數中調用close。這一類用於資源管理的classes在第3章有詳細探討,這兒只要考慮它們的析構函數長相就夠了:
class DBConn { //這個class用來管理DBConnection對象
public:
...
~DBConn() //確保數據庫連接總是會被關閉
{
db.close();
}
private:
DBConnection db;
};
這便允許客戶寫出這樣的代碼:
{ //開啟一個區塊(Block)。
DBConn dbc(DBConnection::create()); //建立DBConnection對象並交給DBConn對象以便管理。通過DBConn的接口使用DBConnection對象。
... //在區塊結束點,DBConn對象被銷毀,因而自動為DBConnection對象調用close。
}
只要調用close成功,一切都美好。但如果該調用導致異常,DBConn析構函數會傳播該異常,也就是允許它離開這個析構函數。那會造成問題,因為那就是拋出了難以駕馭的麻煩。
兩個辦法可以避免這個問題。DBConn的析構函數可以:
方案1、 如果close拋出異常就結束程序,通常調用abort完成:
DBConn::~DBconn()
{
try { db.close(); }
catch(...)
{
abort();
}
}
如果程序遭遇一個“於析構期間發生的錯誤”後無法繼續執行,“強制結束程序”是個合理選項,畢竟它可以阻止異常從析構函數傳播出去(那會導致不明確的行為)。也就是說調用abort可以搶先制“不明確行為”於死地。
方案2、吞下因調用close而發生的異常:
DBConn::~DBConn
{
try{ db.close(); }
catch(...)
{
//制作運轉記錄,記下對close的調用失敗!
}
}
3、進一步方案
一般而言,將異常吞掉是個壞主意,因為它壓制了“某些動作失敗”的重要信息!然而有時候吞下異常也比負擔“草率結束程序”或“不明確行為帶來的風險”好。為了讓這成為一個可行方案,程序必須能夠繼續可靠的執行,即使在遭遇並忽略一個錯誤之後。
這些辦法都不怎麼樣,一個較佳的策略是重新設計DBConn接口,使其客戶有機會對可能出現的問題作出反應。例如DBConn自己可以提供一個close函數,因而賦予客戶一個機會可以處理“因該操作而發生的異常”。
把調用close的責任從DBConn析構函數手上移到DBConn客戶手中,你也許會認為它違反了“讓接口容易被正確使用”的忠告。實際上這污名並不成立。如果某個操作可能在失敗的時候拋出異常,而又存在某種需要必須處理該異常,那麼這個異常必須來自析構函數以外的某個函數。因為析構函數吐出異常就是危險,總會帶來“過早結束程序”或“發生不明確行為”的風險。
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吞下該異常或結束程序,客戶沒有立場抱怨,畢竟他們曾有機會第一手處理問題,而他們選擇了放棄。
請記住:
1.析構函數絕對不要吐出異常,如果一個被析構函數調用的函數可能拋出異常,析構函數應該捕捉任何異常,然後吞下它們(不傳播)或結束程序。
2.如果客戶需要對某個操作函數運行期間拋出的異常作出反應,那麼class應該提供一個普通函數(而非在析構函數中)執行該操作。
作者“wallwind的專欄”