新年了~忙著東奔西跑3天,是時候回歸正常生活了……
條款08:別讓異常逃離析構函數
Prevent exceptions from leavingdestructors
C++ 不禁止但不鼓勵從析構函數引發異常。考慮:
class Widget {
public:
...
~Widget() { ... } // 假設這裡可能吐出一個異常
};
void doSomething()
{
std::vector<Widget> v;
...
} // v在這裡被自動銷毀
當 vector v 被析構時,它有責任析構它包含的所有 Widgets。但假設在那些調用期間,先後有兩個Widgets拋出異常,對於 C++ 來說,這太多了。在兩個異常同時存在的情況下,程序若不是結束執行就是導致不明確行為。在本例中將導致不明確行為,使用標准庫的任何其他容器(如list,set)或TR1的任何容器甚至array,也會出現相同情況。C++ 不喜歡析構函數吐出異常。
如果你的析構函數需要執行一個可能失敗而拋出一個異常的操作,該怎麼辦呢?假設使用一個class負責數據庫連接,為了確保客戶不會忘記在 DBconnection對象上調用 close(),一個合理的想法是創建一個用來管理DBConnection資源的類,並在其析構函數中調用close:
class DBConn { // 這個類用來管理DBConnection對象
public: // objects
...
~DBConn() // 確保數據庫連接總是會被關閉
{ db.close();}
private:
DBConnection db;
};
它允許客戶像這樣編程:
{ // 打開一個區塊(block)
DBConn dbc(DBConnection::create());
// 建立DBConnection並交給DBConn對象以便管理
... // 通過DBConn的接口使用DBConnection對象
} //在區塊結束點,DBConn對象被銷毀,因而自動為DBConnection對象調用close
只要調用 close 成功,一切都美好。但是如果這個調用導致一個異常,DBConn 的析構函數將傳播那個異常,也就是允許它離開析構函數。這就產生了問題,因為析構函數拋出了一個燙手的山芋。
有兩個主要的方法避免這個麻煩。
· Terminatethe program:如果 close 拋出異常就終止程序,一般是通過調用 abort。
DBConn::~DBConn()
{
try { db.close(); }
catch (...) {
制作運轉記錄,記下對close的調用失敗;
std::abort();
}
}
它有一個好處是:阻止異常從析構函數中傳播出去(那會導致不明確的行為)。也就是說,調用 abort 可以預先制“不明確行為”於死地。
· Swallowthe exception:吞下因調用close而發生的異常。在此例中將在第一種方法下去掉abort那句語句。
通常,將異常吞掉是個壞主意,因為它隱瞞了“某些動作失敗”的重要信息!然而,有些時候,吞下異常比冒程序過早終止或不明確行為的風險更可取。程序必須能夠在遭遇到一個錯誤並忽略之後還能繼續可靠地運行,這才能成為一個可行的選擇。
· 析構函數應該永不引發異常。如果析構函數調用了可能拋出異常的函數,析構函數應該捕捉所有異常,然後不傳播它們或者終止程序。
以上方法的問題都在於兩者無法對引起 close 拋出異常的情況做出回應。
一個更好的策略是重新設計 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 的析構函數移交給 DBConn 的客戶(同時在 DBConn 的析構函數中仍內含一個“雙保險調用”)。如果某個操作可能在失敗時拋出異常,而又存在某種需要必須處理該異常,那麼這個異常必須來自析構函數以外的某個函數。這是因為析構函數)引發異常是危險的,永遠都要冒著程序過早終止或 不明確行為的風險。在本例中,讓客戶自己調用 close 並不是強加給他們的負擔,而是給他們一個處理錯誤的機會。他們可以忽略它,依靠 DBConn 的析構函數去調用 close。如果真有錯誤發生,close的確拋出異常而且DBConn吞下該異常或結束程序,客戶沒有立場抱怨,畢竟他們曾有機會第一手處理問題,而他們選擇了放棄。
· 如果客戶需要對某個操作函數運行期間拋出的異常做出反應,那麼類應該提供一個普通函數(非析構函數)執行該操作。
條款09:絕不在構造和析構過程中調用virtual函數
Never call virtual functions duringconstruction or destruction
先概述重點:你不應該在構造或析構期間調用 virtual函數,因為這樣的調用不會如你想象那樣工作,而且會讓你很郁悶。作為 Java 或 C# 程序員,也要更加注意本條款,因為這是C++與它們不相同的一個地方。
假設你有一套模擬股票交易的類繼承體系,例如,購入、出售訂單等。這樣的交易一定要經過審計,所以每一個交易對象被創建,在一個審查日志中就需要創建一個相應的條目。下面是一個看起來似乎合理的解決問題的方法:
class Transaction { // 所有交易的基類
public:
Transaction();
virtual void logTransaction() const = 0; // 做出一份因類型不同而不同的日志記錄
...
};
Transaction::Transaction() // 基類構造函數之實現
{
...
logTransaction(); // 最後動作是志記這筆交易
}
class BuyTransaction: public Transaction { //derived class
public:
virtual void logTransaction() const;
...
};
class SellTransaction: public Transaction {// derived class
public:
virtual void logTransaction() const;
...
};
考慮執行這行代碼時會發生什麼:
BuyTransaction b;
很明顯一個 BuyTransaction 的構造函數會被調用,但是首先,一個 Transaction 的 構造函數必須先被調用,派生類對象中的基類成分先於派生類自身成分被構造之前構造。Transaction 的構造函數的最後一行調用 virtual函數 logTransaction,,被調用的 logTransaction 版本是在 Transaction 中的那一個,而不是 BuyTransaction 中的那一個,即使被創建的對象類型是 BuyTransaction。基類構造期間,virtual函數從來不會向下匹配到派生類。
更根本的原因:在一個派生類對象的基類構造期間,對象的類型是基類,而不是派生類。不僅 virtual函數會解析到基類,而且若使用到 runtime type information(運行時類型信息)的語言構件(例如,dynamic_cast和 typeid),也會將那個對象視為基類類型。本例中,當 Transaction 的 構造函數正打算初始化一個 BuyTransaction對象的基類部分時,該對象的類型是Transaction 。這樣的對待是合理的:這個對象的 BuyTransaction專屬部分還沒有被初始化,所以最安全的做法是視它們不存在。對象在派生類構造函數開始執行前不會成為一個派生類對象。同樣的道理也適用於析構函數。
在上面的示例代碼中,Transaction 的構造函數造成了對一個 virtual函數的直接調用,這很明顯而且容易看出違反本條款。這一違背是如此顯見,以致一些編譯器會給出一個關於它的警告(另一些則不會)。
在構造或析構期間調用 virtual函數的問題並不總是如此容易被察覺。如果 Transaction 有多個構造函數,每一個都必須完成一些相同的工作,為避免代碼重復將共通的初始化代碼,包括對 logTransaction 的調用,放入一個初始化函數中,叫做 init:
class Transaction {
public:
Transaction()
{ init(); } // 調用non-virtual...
virtual void logTransaction() const = 0;
...
private:
void init()
{
...
logTransaction(); // 這裡調用virtual!
}
};
這個代碼在概念上和早先那個版本相同,但是它更陰險,因為一般來說它會躲過編譯器和連接程序的抱怨。其實還是在構造函數內調用了virtual。避免這個問題的唯一辦法就是確保你的構造函數或析構函數決不在被創建或析構的對象上調用 virtual函數,而它們所調用的所有函數也服從同樣的約束。
如何確保在每一次 Transaction繼承體系中的一個對象被創建時,都會調用 logTransaction 的正確版本呢?將 Transaction 中的 logTransaction 轉變為一個 non-virtual函數,然後要求派生類構造函數將必要的信息傳遞給 Transaction 構造函數,而後那個函數就可以安全地調用 non-virtual的 logTransaction。如下:
class Transaction {
public:
explicit Transaction(const std::string& logInfo);
void logTransaction(conststd::string& logInfo) const;
// 如今是個non-virtual函數
...
};
Transaction::Transaction(const std::string& logInfo)
{
...
logTransaction(logInfo); //如今是個non-virtual函數
}
class BuyTransaction: public Transaction {
public:
BuyTransaction( parameters )
: Transaction(createLogString( parameters ))
{ ... } // 將log信息傳遞給基類構造函數
...
private:
static std::string createLogString(parameters );
};
換句話說,由於你不能在基類的構造過程中使用 virtual函數向下調用,你可以改為讓派生類將必要的構造信息上傳給基類構造函數作為補償。
在此例中,注意 BuyTransaction 中那個 private static 函數 createLogString 的使用。使用一個輔助函數創建一個值傳遞給基類構造函數,通常比通過在成員初值列給基類它所需數據更加便利(也更加具有可讀性)。將那個函數設置為static,就不會有偶然觸及到一個新生的 BuyTransaction object對象的仍未初始化的數據成員的危險。
· 在構造或析構期間不要調用 virtual函數,因為這樣的調用從不下降至派生類(比起當前執行構造函數和析構函數的那層)。
摘自 pandawuwyj的專欄