effect C++ 絕不在構造和析構過程中調用virtual函數

base class構造期間virtual函數絕不會下降到derived classes階層

用class 繼承體系,用來塑模股市交易如買進、賣出訂單。每創建一個交易對象,審計日誌中需要創建一筆適當的記錄。

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構造函數一定會被更早調用;是的,derived class對象內的base class 成分會在derived class自身成分被構造函數之前構造妥當。Transaction構造函數的最後一行調用virtual函數logTransaction, 被調用的logTransaction是Transaction內的版本,不是BuyTransaction內的版本。base class構造期間virtual函數絕不會下降到derived classes階層

由於base class 構造函數執行更早於derived class 構造函數,當base class 構造函數執行時 derived class 的成員變量尚未初始化。如果此期間調用的virtual函數下降至derived層,(derived class 的函數必然取用local 成員變量,而那些成員變量尚未初始化)會導致不明確行爲。


在derived class 對象的base class構造期間,對象的類型是base class 而不是derived class。不止virtual 函數爲被編譯器解析至base class,若使用運行期類型信息,也會把對象視爲base class 類型。

當Transaction 構造函數正準備初始化”BuyTransaction對象內的base class成分“時,該對象的類型是Transaction。

這個對象內的”BuyTransaction專屬成分“尚未被初始化,所以在面對它們,最安全的做法就是視它們不存在。對象在derived class構造函數開始執行前不會成爲一個drivied class對象

相同的道理也適用於析構函數。一旦derived class析構函數開始執行,對象內的derived class 成員變量便呈現未定義值。進入base class 析構函數後對象就成爲一個base class 對象,而C++ 的任何部分包括virtual 函數,dynamic_casts等等也就那麼看待它。


如果Transaction 有多個構造函數,每個都需執行某些相同工作,那麼避免代碼重複的一個優秀的做法是把共同的初始化代碼(其中包括對logTransaction的調用)放進一個初始化函數如init內:

public:
Transaction()
 { init();}  	//調用non-virtual...
 virtual void logTransaction()const=0;
  ...
private:
   void init()
   {
     ....
     logTransaction();  //這裏調用virtual
   }
}

此時,由於logTransaction是Transaction內的一個pure virtual 函數,當pure virtual函數被調用,大多執行系統會中止程序。

然而如果logTransaction 是個正常的(也就是impure)virtual 函數並在Trabsaction 內帶有一份實現代碼,該版本就會被調用,從而建立一個derived class對象時會調用錯誤版本的logTransaction。


避免:確定你的構造函數和析構函數都沒有(在對象被創建和被銷燬期間)調用virtual函數,而它們調用的所有函數也都服從同一約束。

在Transaction構造函數內對着對象調用virtual函數是一種錯誤的做法。


在class Transaction 內將logTransaction 函數改爲non-virtual ,然後要求derived class 構造函數傳遞必要信息給Transaction 構造函數,而後那個構造便可安全地調用non_virtual logTransaction.

 

class Transaction{
public:
  explicit Transaction(const std::string&logInfo);
  void logTransaction(const std::string&logInfo)const;//non-virtual函數
  ...
};
Transaction:: Transaction(const std::string&logInfo)
{
  ...
  logTransaction(logInfo);
}
class BuyTransaction:public Transaction{
public:
  BuyTransaction(parameters):Transaction(createLogString(parameters))//將log信息傳給base class函數
  {...}
  ...
  private:
  static std::string createLogString(parameters);
};

你無法使用virtual函數從base classes向下調用。只能在構造期間令derived classes 將必要的構造信息向上傳遞至base class構造函數 加以彌補

注意BuyTransaction 內的private static 函數createLogString。

比起在成員初值列內給予base class 所需數據,利用輔助函數創建一個值給base class 構造函數往往比較方便,也比較可讀。令此函數爲static,也就不可能意外指向”初期未成熟之BuyTransaction對象內尚未初始化的成員變量”,正是因爲“那些成員變量處於未定義狀態“,所以,"在base class 構造和析構期間調用的virtual函數不可下降至derived classes"。


在構造和析構期間不要調用virtual函數,因爲這類調用從不下降至derived class(比起當前執行構造函數和析構函數的那層)。


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