# Item 9: 绝不要在 construction(构造)或 destruction(析构)期间调用 virtual functions(虚拟函数)
作者:Scott Meyers
译者:fatalerror99 (iTePub's Nirvana)
发布:http://blog.csdn.net/fatalerror99/
我以这个概述开始:你不应该在 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(派生类)。
- Preface(前言)
- Introduction(导言)
- Terminology(术语)
- Item 1: 将 C++ 视为 federation of languages(语言联合体)
- Item 2: 用 consts, enums 和 inlines 取代 #defines
- Item 3: 只要可能就用 const
- Item 4: 确保 objects(对象)在使用前被初始化
- Item 5: 了解 C++ 为你偷偷地加上和调用了什么函数
- Item 6: 如果你不想使用 compiler-generated functions(编译器生成函数),就明确拒绝
- Item 7: 在 polymorphic base classes(多态基类)中将 destructors(析构函数)声明为 virtual(虚拟)
- Item 8: 防止因为 exceptions(异常)而离开 destructors(析构函数)
- Item 9: 绝不要在 construction(构造)或 destruction(析构)期间调用 virtual functions(虚拟函数)
- Item 10: 让 assignment operators(赋值运算符)返回一个 reference to *this(引向 *this 的引用)
- Item 11: 在 operator= 中处理 assignment to self(自赋值)
- Item 12: 拷贝一个对象的所有组成部分
- Item 13: 使用对象管理资源
- Item 14: 谨慎考虑资源管理类的拷贝行为
- Item 15: 在资源管理类中准备访问裸资源(raw resources)
- Item 16: 使用相同形式的 new 和 delete
- Item 17: 在一个独立的语句中将 new 出来的对象存入智能指针
- Item 18: 使接口易于正确使用,而难以错误使用
- Item 19: 视类设计为类型设计
- Item 20: 用 pass-by-reference-to-const(传引用给 const)取代 pass-by-value(传值)
- Item 21: 当你必须返回一个对象时不要试图返回一个引用
- Item 22: 将数据成员声明为 private
- Item 23: 用非成员非友元函数取代成员函数
- Item 24: 当类型转换应该用于所有参数时,声明为非成员函数
- Item 25: 考虑支持不抛异常的 swap
- Item 26: 只要有可能就推迟变量定义
- Item 27: 将强制转型减到最少
- Item 28: 避免返回对象内部构件的“句柄”
- Item 29: 争取异常安全(exception-safe)的代码
- Item 30: 理解 inline 化的介入和排除
- Item 31: 最小化文件之间的编译依赖
- Item 32: 确保 public inheritance 模拟 "is-a"
- Item 33: 避免覆盖(hiding)“通过继承得到的名字”
- Item 34: 区分 inheritance of interface(接口继承)和 inheritance of implementation(实现继承)
- Item 35: 考虑可选的 virtual functions(虚拟函数)的替代方法
- Item 36: 绝不要重定义一个 inherited non-virtual function(通过继承得到的非虚拟函数)
- Item 37: 绝不要重定义一个函数的 inherited default parameter value(通过继承得到的缺省参数值)
- Item 38: 通过 composition(复合)模拟 "has-a"(有一个)或 "is-implemented-in-terms-of"(是根据……实现的)
- Item 39: 谨慎使用 private inheritance(私有继承)
- Item 40: 谨慎使用 multiple inheritance(多继承)
- Item 41: 理解 implicit interfaces(隐式接口)和 compile-time polymorphism(编译期多态)
- Item 42: 理解 typename 的两个含义
- Item 43: 了解如何访问 templatized base classes(模板化基类)中的名字
- Item 44: 从 templates(模板)中分离出 parameter-independent(参数无关)的代码
- Item 45: 用 member function templates(成员函数模板) 接受 "all compatible types"(“所有兼容类型”)
- Item 46: 需要 type conversions(类型转换)时在 templates(模板)内定义 non-member functions(非成员函数)
- Item 47: 为类型信息使用 traits classes(特征类)
- Item 48: 感受 template metaprogramming(模板元编程)
- Item 49: 了解 new-handler 的行为
- Item 50: 领会何时替换 new 和 delete 才有意义
- Item 51: 编写 new 和 delete 时要遵守惯例
- Item 52: 如果编写了 placement new,就要编写 placement delete
- 附录 A. 超越 Effective C++
- 附录 B. 第二和第三版之间的 Item 映射