「生活可以更简单, 欢迎来到我的开源世界」
  1. 类构造和析构期间不会发生向下调用
  2. 将必要的构造信息向上传递
  3. 总结
item9-绝不在构造和析构过程中调用virtual函数
2021-01-01
」 「

item9-绝不在构造和析构过程中调用virtual函数

类构造和析构期间不会发生向下调用

假设你有一套 class 继承体系用来模拟股票交易的类继承体系,例如,购入订单,出售订单等:

//@ 基类
class Transaction {
public:
Transaction();
virtual void logTransaction() const = 0;
...
};

Transaction::Transaction()
{
...
logTransaction();
}

//@ 买家派生类
class BuyTransaction : public Transaction {
public:
virtual void logTransaction() const;
...
};

//@ 卖家派生类
class SellTransaction : public Transaction {
public:
virtual void logTransaction() const;
...
};

当执行:

BuyTransaction b;

base class 的构造函数先于 derived class 的构造函数执行,而 base class 的构造函数里调用了一个 virtual 函数,此时该虚函数调用的是 base class 版本的,而不是 derived class 版本的,即使目前即将建立的类型是 derived class的。

这样的原因是:由于 base class 的构造函数的执行早于 derived class 构造函数,当 base class 构造函数执行时, derived class 的成员变量尚未初始化。如果此期间调用的 virtual 函数下降至 derived class 阶层,则函数几乎必然会用到 local 成员变量,而那些变量尚未初始化,将会产生未定义行为。

更根本的原因是:在derived class 对象的 base class构造期间,对象的类型是base class 而不是 derived class,不只 virtual 函数会被编译器解析至 base class,若使用运行期类型信息,也会把对象视为 base class类型。对象在 derived class构造函数开始执行前不会成为一个 derived class对象。

相同的道理适用于析构函数。一旦 derived class 析构函数开始执行,对象内的derived class 成员变量便呈现未定义值,所以C++视它们仿佛不再存在。进入 base class析构函数后对象成为一个 base class对象,而C++的任何部分包括 virtual 函数、dynamic_casts 等等也就那么看待它。

一般构造/析构函数里使用virtual 函数编译器会发出警告。因为 logTransaction 函数是个纯虚函数,除非被定义(不太有希望,但是可能)否则程序无法连接。

侦测构造函数或析构函数运行期间是否调用 virtual 函数并不是总是这般轻松,当因为存在多个构造函数都执行某些相同的工作时,为避免代码重复而将共同代码(包括调用 virtual函数)放进一个初始化函数 init 内供构造函数调用。

class Transaction {                              
public:
Transaction(){
init();
}
virtual void logTransaction() const {
std::cout << "base class" << std::endl;
}
private:
void init(){
...
logTransaction();
}
};

类中构造函数间接调用了virtual函数,通常不会引发任何编译器和连接器的报错或警告,但实际仍然存在相应问题。

将必要的构造信息向上传递

无法使用 virtual 函数从base class 向下调用,在构造期间,可以通过“令 derived class 将必要的构造信息向上传递至base class构造函数”作为替换和弥补策略。

首先将base class 内的 logTransaction 函数改为 non-virtual,然后要求 derived class 构造函数传递必要的信息给 base class 构造函数,而后 base class构造函数便可安全地调用 non-virtual logTransaction。

class Transaction {
public:
explicit Transaction(const std::string& logInfo);

void logTransaction(const std::string& logInfo) const; // now a non-
// virtual func
...
};

Transaction::Transaction(const std::string& logInfo)
{
...
logTransaction(logInfo); // now a non-
} // virtual call

class BuyTransaction: public Transaction {
public:
BuyTransaction(parameters)
: Transaction(createLogString( parameters )) // pass log info
{ ... } // to base class
... // constructor

private:
static std::string createLogString( parameters );
};

注意private static函数 createLogString 的运用,比起在初始化列表内给予 base class 所需数据,利用辅助构造函数创建一个值传给 base class构造函数往往比较方便(也比较可读)。

总结

<⇧>