More Effective C++ 11:禁止異常信息傳遞到析構函數外

棧展開(stack-unwinding)

拋出異常時,將暫停當前函數的執行,開始查找匹配的catch子句。首先檢查throw本身是否在try塊內部,如果是,檢查與該try相關的catch子句,看是否可以處理該異常。如果不能處理,就退出當前函數,並且釋放當前函數的內存並銷燬局部對象,繼續到上層的調用函數中查找,直到找到一個可以處理該異常的catch。這個過程稱爲棧展開。

有兩種情況下會調用析構函數:
1.在正常情況下刪除一個對象,例如對象超出了作用域或被顯式地delete
2.異常傳遞的堆棧輾轉開解(stack-unwinding)過程
中,由異常處理系統刪除一個對象。

在上述兩種情況下,調用析構函數時異常可能處於激活狀態也可能沒有處於激活狀態。遺憾的是沒有辦法在析構函數內部區分出這兩種情況。
如果在一個異常被激活的同時,析構函數也拋出異常,並導致程序控制權轉移到析構函數外,C++將調用terminate函數,它終止你程序的運行,而且是立即終止,甚至連局部對象都沒有被釋放。
考慮以下的類:

class Session 
{ 
public: 
	Session(); 
	~Session(); 
	... 
private: 
	static void logCreation(Session *objAddr); 
	static void logDestruction(Session *objAddr); 
};

函數 logCreationlogDestruction 被分別用於記錄對象的建立與釋放。 我們因此可以這樣編寫 Session 的析構函數:

Session::~Session() 
{ 
	logDestruction(this); 
}

如果 logDestruction 拋出一個異常,會發生什麼事呢?
異常沒有被 Session 的析構函數捕獲住,所以它被傳遞到析構函數的調用者那裏。但是如果析構函數本身的調用就是源自於某些其它異常的拋出,那麼 terminate 函數將被自動調用,徹底終止你的程序。

那麼事態果真嚴重到了必須終止程序運行的地步了麼?如果沒有,你必須防止在 logDestruction 內拋出的異常傳遞到 Session 析構函數的外面。唯一的方法是用trycatch blocks。一種很自然的做法會這樣編寫函數:

Session::~Session() 
{ 
	try 
	{ 
		logDestruction(this); 
	} 
 	catch (...) 
 	{ 
		 cerr << "Unable to log destruction of Session object " 
		 << "at address " 
		 << this 
		 << ".\n"; 
	} 
}

但是這樣做並不比你原來的代碼安全。如果在 catch 中調用 operator<<時導致一個異常被拋出,我們就又遇到了老問題,一個異常被轉遞到 Session 析構函數的外面
我們可以在 catch 中放入 try,但是這總得有一個限度,否則會陷入循環。因此我們在釋放 Session 時必須忽略掉所有它拋出的異常:

Session::~Session() 
{ 
	 try 
	 { 
	 	logDestruction(this); 
	 } 
	 catch (...) { } 
}

catch 表面上好像沒有做任何事情,這是一個假象,實際上它阻止了任何從logDestruction 拋出的異常被傳遞到 session 析構函數的外面
我們現在能高枕無憂了,無論 session 對象是不是在堆棧退棧(stack unwinding)中被釋放,terminate 函數都不會被調用。

不允許異常傳遞到析構函數外面還有第二個原因:
如果一個異常被析構函數拋出而沒有在函數內部捕獲住,那麼析構函數就不會完全運行。
如果析構函數不完全運行,它就無法完成希望它做的所有事情。

例如,我們對 session 類做一個修改,在建立 session 時啓動一個數據庫事務(database transaction),終止 session時結束這個事務:

Session::Session() // 爲了簡單起見,, 
{ // 這個構造函數沒有 
 // 處理異常 
	 logCreation(this); 
	 startTransaction(); // 啓動 database transaction 
} 
Session::~Session() 
{ 
	 logDestruction(this); 
	 endTransaction(); // 結束 database transaction 
}

如果在這裏 logDestruction 拋出一個異常,在 session 構造函數內啓動的 transaction就沒有被終止。我們也許能夠通過重新調整 session 析構函數內的函數調用順序來消除問題,但是如果 endTransaction 也拋出一個異常,我們除了回到使用 trycatch 外,別無選擇。

總結

禁止異常傳遞到析構函數外,兩個原因:
1.能夠在異常轉遞的堆棧輾轉開解(stack-unwinding)的過程中,防止 terminate 被調用。
2.幫助確保析構函數總能完成我們希望它做的所有事情

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章