Effective C++ 條款08_不止於此

weixin_48033173發表於2020-10-09

別讓異常逃離解構函式

C++ 並不禁止解構函式吐出異常,但它也不鼓勵這樣做。
考慮下面程式碼:

class Widget {
public: 
	...
	~Widget( ) {...}  // 假設這裡吐出一個異常
};
void soSomething()
{
	std::vector<Widget> vec;
	...
}			// vec 在這裡被自動銷燬

當 vector vec 被銷燬,它有責任銷燬其內含的所有 Widgets。假設 vec 內含10個Widgets,而在析構第一個元素期間,有一個異常被丟擲。其他九個 Widgets 還是應該被銷燬(否則它們儲存的任何資源都會發生洩漏),因此 vec 應該呼叫它們各個解構函式。但假設在那些呼叫期間,第二個 Widget 解構函式又丟擲異常。現在有兩個同時作用的異常,這對 C++ 而言太多了。在兩個異常同時存在的情況下,程式若不是結束就是導致不明確行為。本例會導致不明確行為。
這很容易理解,但如果你的解構函式必須執行一個動作,而該動作可能會在失敗是丟擲異常,該咋辦呢?舉個例子,假設你使用一個 class 來負責資料庫連線:

class DBConnection {
public:
	...
	static DBConnection create();  //返回 DBConnection 物件
	void close(); 		// 關閉聯機;失敗則丟擲異常
};
  • 為了確保使用者不忘記在 DBConnection 物件身上呼叫 close() ,我們可以這樣做,建立一個用來管理 DBConnection 資源的 class,並在其解構函式中呼叫 close。
    程式碼如下:
class DBConn {
	DBConnection db;
public:
	...
	~DBConn(){
	db.close();
}
};

這樣便允許使用者寫出這樣的程式碼:

{				// 開啟一個區塊(block)。
DBConn dbc(DBConnection::create()); // 建立 DBConnection 物件並交給 DBConn 物件以便管理。
					// 通過 DBConn 的介面使用 DBConnection 物件。	
					// 在區塊結束點,DBConn 物件被銷燬,因而自動為 DBConnection 物件呼叫 close
...
}

只要呼叫 close 成功,一切都美好,但如果該呼叫導致異常,DBConn 解構函式會傳播該異常,也就是允許它離開這個解構函式。那樣就會造成問題,因為那是丟擲了難以駕馭的麻煩。
下面給出兩種辦法解決這個問題

  • 如果 close 丟擲異常就結束程式。通常通過呼叫 abort 完成:
DBConn::~DBConn( ){
	try { db.close(); }
	catch ( ... ){
		// 製作運轉記錄,記下對 close 的呼叫失敗
		std::abort();  // 使用 abort 可以搶先制 “ 不明確 ” 行為於死地,也就是強迫結束程式
		}
}
  • 吞下因呼叫 close 而發生的異常:
DBConn::~DBConn( ){
	try { db.close(); }
	catch ( ... ){
		// 製作運轉記錄,記下對 close 的呼叫失敗
		}
}

一般而言,這樣做是一個壞主意,因為它壓制了 “ 某些動作失敗 ” 的重要資訊!然而有時候吞下異常也比負擔 “ 草率結束程式 ” 或 “ 不明確行為帶來的風險 ” 好。
顯然這些方法都不具有吸引力。問題在於兩者都無法對 “ 導致 close丟擲異常 ” 的情況做出反應。
一種較佳策略是重新設計 DBConn 介面,使使用者有機會對可能出現的問題做出反應:

class DBConn {
public:
	void close (){ 			// 供使用者使用的新函式
	db.close();
	closed = true;
}
~DBConn(){
	if(!closed) {
	try {			// 關閉連線(如果使用者不那麼做(呼叫close())的話)
	db.close();
 	}
 	catch(...){
	// 製作運轉記錄,記下對 close 的呼叫失敗
	...
	}
	}
}
private:
	DBConnection db;
	bool closed;
};

把呼叫 close 的責任從 DBConn 解構函式手上移到 DBConn 使用者手上,讓使用者有機會第一手處理問題。

最後請記住:

  • 解構函式絕對不要吐出異常。如果一個被解構函式呼叫的函式可能丟擲異常,解構函式應該捕捉任何異常,然後吞下它們(不傳播)或結束程式。
  • 如果使用者需要對某個操作函式執行期間丟擲的異常作出反應,那麼 class 應該提供一個普通函式(而非在解構函式中)執行該操作。

相關文章