我以这个概述开始:你不应该在 construction(构造)或 destruction(析构)期间调用 virtual functions(虚拟函数),因为这样的调用不会如你想象那样工作,而且它们做的事情保证会让你很郁闷。如果你转为 Java 或 C# 程序员,也请你密切关注本 Item,因为在 C++ 急转弯的地方,那些语言也紧急转了一个弯。
假设你有一套模拟股票交易的 class hierarchy(类继承体系),例如,购入订单,出售订单等。对于这样的交易来说可供审查是非常重要的,所每次一个交易对象被创建,在一个审查日志中就需要创建一个相应的条目。下面是一个看起来似乎合理的解决问题的方法:
class Transaction { // base class for all
public: // transactions
Transaction();
virtual void logTransaction() const = 0; // make type-dependent
// log entry
...
};
Transaction::Transaction() // implementation of
{ // base class ctor
...
logTransaction(); // as final action, log this
} // transaction
class BuyTransaction: public Transaction { // derived class
public:
virtual void logTransaction() const; // how to log trans-
// actions of this type
...
};
class SellTransaction: public Transaction { // derived class
public:
virtual void logTransaction() const; // how to log trans-
// actions of this type
...
};
考虑执行这行代码时会发生什么:
BuyTransaction b;
很明显一个 BuyTransaction 的 constructor(构造函数)会被调用,但是首先,一个 Transaction 的 constructor(构造函数)必须先被调用,derived class objects(派生类对象)中的 base class parts(基类构件)先于 derived class parts(派生类构件)被构造。Transaction 的 constructor(构造函数)的最后一行调用 virtual functions(虚拟函数) logTransaction,但是结果会让你大吃一惊,被调用的 logTransaction 版本是在 Transaction 中的那一个,而不是 BuyTransaction 中的那一个——即使被创建的 object (对象)类型是 BuyTransaction。base class construction(基类构造)期间,virtual functions(虚拟函数)从来不会 go down(向下匹配)到 derived classes(派生类)。取而代之的是,那个 object (对象)的行为好像它就是 base type(基类型)。非正式地讲,base class construction(基类构造)期间,virtual functions(虚拟函数)被禁止。
这个表面上看起来匪夷所思的行为存在一个很好的理由。因为 base class constructors(基类构造函数)在 derived class constructors(派生类构造函数)之前执行,当 base class constructors(基类构造函数)运行时,derived class data members(派生类数据成员)还没有被初始化。如果 base class construction(基类构造)期间 virtual functions(虚拟函数)的调用 went down(向下匹配)到 derived classes(派生类),derived classes(派生类)的函数差不多总会涉及到 local data members(局部数据成员),但是那些 data members(数据成员)至此还没有被初始化。这就会为 undefined behavior(未定义行为)和通宵达旦的调试噩梦开了一张通行证。调用涉及到一个 object(对象)还没有被初始化的构件自然是危险的,所以 C++ 告诉你此路不通。
实际上还有比这更基本的原理。在一个 derived class object(派生类对象)的 base class construction(基类构造)期间,object(对象)的类型是 base class(基类)的类型。不仅 virtual functions(虚拟函数)会解析到 base class(基类),而且用到 runtime type information(运行时类型信息)的语言构件(例如,dynamic_cast(参见 Item 27)和 typeid),也会将那个 object(对象)视为 base class type(基类类型)。在我们的例子中,当 Transaction 的 constructor(构造函数)运行到初始化一个 BuyTransaction object(对象)的 base class(基类)部分时,那个 object(对象)的是 Transaction 类型。C++ 的每一个构件将以如下眼光来看待它,而且这种看法是合理的:这个 object(对象)的 BuyTransaction-specific 的构件还没有被初始化,所以对它们视若无睹是最安全的。直到 derived class constructor(派生类构造函数)的执行开始之前,一个 object(对象)不会成为一个 derived class object(派生类对象)。
同样的推理也适用于 destruction(析构)。一旦 derived class destructor(派生类析构函数)运行,这个 object(对象)的 derived class data members(派生类数据成员)就呈现为未定义的值,所以 C++ 就将它们视为不再存在。在进入 base class destructor(基类析构函数)时,这个 object(对象)就成为一个 base class object(基类对象),C++ 的所有构件—— virtual functions(虚拟函数),dynamic_casts 等——都以此看待它。
在上面的示例代码中,Transaction 的 constructor(构造函数)造成了对一个 virtual functions(虚拟函数)的一次直接调用,是对本 Item 的指导建议的显而易见的违背。这一违背是如此显见,以致一些编译器会给出一个关于它的警告。(另一些则不会。参见 Item 53 对于警告的讨论。)即使没有这样的一个警告,这个问题也几乎肯定会在运行之前暴露出来,因为 logTransaction 函数在 Transaction 中是 pure virtual(纯虚拟)的。除非它被定义(不太可能,但确实可能——参见 Item 34),否则程序将无法连接:连接程序无法找到 Transaction::logTransaction 的必要的实现。
在 construction(构造)或 destruction(析构)期间调用 virtual functions(虚拟函数)的问题并不总是如此容易被察觉。如果 Transaction 有多个 constructors(构造函数),每一个都必须完成一些相同的工作,软件工程为避免代码重复,将共通的 initialization(初始化)代码,包括对 logTransaction 的调用,放入一个 private non-virtual initialization function(私有非虚拟初始化函数)中,叫做 init:
class Transaction {
public:
Transaction()
{ init(); } // call to non-virtual...
virtual void logTransaction() const = 0;
...
private:
void init()
{
...
logTransaction(); // ...that calls a virtual!
}
};
这个代码在概念上和早先那个版本相同,但是它更阴险,因为一般来说它会躲过编译器和连接程序的抱怨。在这种情况下,因为 logTransaction 在 Transaction 中是 pure virtual(纯虚的),在 pure virtual(纯虚)被调用时,大多数 runtime systems(运行时系统)会异常中止那个程序(一般会对此结果给出一条消息)。然而,如果 logTransaction 在 Transaction 中是一个 "normal" virtual function(“常规”虚拟函数)(也就是说,not pure virtual(非纯虚拟的)),而且带有一个实现,那个版本将被调用,程序会继续一路小跑,让你想象不出为什么在 derived class object(派生类对象)被创建的时候会调用 logTransaction 的错误版本。避免这个问题的唯一办法就是确保你的 constructors(构造函数)或 destructors(析构函数)决不在被创建或析构的 object(对象)上调用 virtual functions(虚拟函数),它们所调用的全部函数也要服从同样的约束。
但是,你如何确保在每一次 Transaction hierarchy(继承体系)中的一个 object(对象)被创建时,都会调用 logTransaction 的正确版本呢?显然,在 Transaction constructor(s)(构造函数)中在这个 object(对象)上调用 virtual functions(虚拟函数)的做法是错误的。
有不同的方法来解决这个问题。其中之一是将 Transaction 中的 logTransaction 转变为一个 non-virtual function(非虚拟函数),这就需要 derived class constructors(派生类构造函数)将必要的日志信息传递给 Transaction constructor(构造函数)。那个函数就可以安全地调用 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 );
};
换句话说,由于你不能在 base classes(基类)的 construction(构造)过程中使用 virtual functions(虚拟函数)向下匹配,你可以改为让 derived classes(派生类)将必要的构造信息上传给 base class constructors(基类构造函数)作为补偿。
在此例中,注意 BuyTransaction 中那个 (private) static 函数 createLogString 的使用。使用一个辅助函数创建一个值传递给 base class constructors(基类构造函数),通常比通过在 member initialization list(成员初始化列表)给 base class(基类)它所需要的东西更加便利(也更加具有可读性)。将那个函数做成 static,就不会有偶然触及到一个新生的 BuyTransaction object(对象)的 as-yet-uninitialized data members(仍未初始化的数据成员)的危险。这很重要,因为实际上那些 data members(数据成员)处在一个未定义状态,这就是为什么在 base class(基类)construction(构造)和 destruction(析构)期间调用 virtual functions(虚拟函数)不能首先向下匹配到 derived classes(派生类)的原因。
Things to Remember
- 在 construction(构造)或 destruction(析构)期间不要调用 virtual functions(虚拟函数),因为这样的调用不会转到比当前执行的 constructor(构造函数)或 destructor(析构函数)所属的 class(类)更深层的 derived class(派生类)。