首先先拋出結論:
- 析構函數絕對不要拋出異常。如果在一個析構函數中某個函數可能調用失敗,拋出異常,這個析構函數應該捕捉這個異常,然後“吞下”他們,或者結束程序。
- 如果客戶端需要對某個操作函數運行期間拋出的異常做出反應,那麼這個 class 應該提供一個普通函數接口,而不是在析構函數中自己處理異常。
假如有以下的代碼片段:
class Widget {
public:
...
~Widget() { } //假設拋出異常
};
void DoSomething()
{
std::vector<Widget> v;
...
}
當 vector 容器被銷燬的時候,它是有責任銷燬它內含的所有的 Widget 對象的。但是假如,v 在析構第一個 Widget 對象的時候,Widget 對象的析構函數拋出異常,但是其他的 Widget 對象仍然需要正常被銷燬。如果多個 Widget 對象都拋出異常,這個時候就會造成程序不是結束執行就是會導致不明確行爲。
那如果在析構函數中必須要執行某個動作,但是這個動作又可能會在失敗的時候拋出異常呢?比如說下面這一種情況:
有一個 DBConnection 類負責與數據庫的連接:
class DBConnection {
public:
static DBConnection create();
void close();
//...
};
然後又一個 DBConnectionManager 類負責管理 DBConnection 對象的連接狀態,爲了保證在用戶在忘記調用 DBConnection 對象的 close() 函數的時候,數據庫的連接也能夠正確的關閉,一個合理的想法就是在 DBConnectionManager 類的析構函數中調用 DBConnection 的 close() 函數,大概是這個樣子:
class DBConnectionManager {
public:
DBConnectionManager(DBConnection db)
{
dbConnection = db;
}
~DBConnectionManager()
{
dbConnection.close();
}
private:
DBConnection dbConnection;
};
這個時候就會存在一個問題:如果 DBConnection 的 close 函數拋出異常,DBConnectionManager 的析構函數就會傳播這個異常,也就是會允許程序離開這個析構函數,這個時候就會造成潛在的問題。
有兩個辦法可以避免這個問題:
第一個就是讓這個析構函數把異常給“吞掉”,但是有一個前提條件就是在這個析構函數把異常“吞掉”之後也要保證程序能夠繼續可靠的執行。
~DBConnectionManager()
{
try {
dbConnection.close();
}
catch (std::exception)
{
//記錄調用失敗
}
}
第二個解決辦法就是在拋出異常的時候就強行結束程序,阻止異常從析構函數中傳播出去,產生不明確行爲。
~DBConnectionManager()
{
try {
dbConnection.close();
}
catch (std::exception)
{
//記錄調用失敗
std::abort();
}
}
前兩者的解決方案都會導致客戶端無法對“導致 close 拋出異常” 的情況做出反應,所以一個更好的解決方案是重新設計 DBConnectionManager 的接口,使得客戶端能夠對 close 拋出的異常做出反應,也就是提供客戶端手動調用 close 函數的接口,但是爲防止客戶端忘記手動關閉,又會在其析構函數中調用 close 函數。這樣的話,即使真的有錯誤發生,也是客戶端“活該”。(都給你接口了讓你手動調用關閉,誰讓你忘記的,就不要怪我自己處理的時候出錯咯)
class DBConnectionManager {
public:
DBConnectionManager(DBConnection db)
{
dbConnection = db;
}
void closeConnection()
{
dbConnection.close();
hasClosed = true;
}
~DBConnectionManager()
{
if (!hasClosed)
{
try {
dbConnection.close();
}
catch (std::exception)
{
//記錄調用失敗
}
}
}
private:
DBConnection dbConnection;
bool hasClosed;
};