Effective C++手册Item 7: 在 polymorphic base classes(多态基类)中将 destructors(析构函数)声明为 virtual(虚拟)

Item 7: 在 polymorphic base classes(多态基类)中将 destructors(析构函数)声明为 virtual(虚拟)

作者:Scott Meyers

译者:fatalerror99 (iTePub's Nirvana)

发布:http://blog.csdn.net/fatalerror99/

有很多方法取得时间,所以有必要建立一个 TimeKeeper base class(基类),并为不同的计时方法建立 derived classes(派生类):

class TimeKeeper {
public:
  TimeKeeper();
  ~TimeKeeper();
  ...
};
class AtomicClock: public TimeKeeper { ... };
class WaterClock: public TimeKeeper { ... };
class WristWatch: public TimeKeeper { ... };

很多客户只是想简单地取得时间而不关心如何计算的细节,所以一个 factory function(工厂函数)——返回 a base class pointer to a newly-created derived class object(一个指向新建派生类对象的基类指针)的函数——可以被用来返回一个指向 timekeeping object(计时对象)的指针:

TimeKeeper* getTimeKeeper();       // returns a pointer to a dynamic-
                                   // ally allocated object of a class
                                   // derived from TimeKeeper

与 factory function(工厂函数)的惯例一致,getTimeKeeper 返回的 objects(对象)是建立在 heap(堆)上的,所以为了避免泄漏内存和其它资源,每一个返回的 objects(对象)被完全 deleted 是很重要的。

TimeKeeper *ptk = getTimeKeeper();  // get dynamically allocated object
                                    // from TimeKeeper hierarchy
...                                 // use it
delete ptk;                         // release it to avoid resource leak

Item 13 解释了为什么依赖客户执行删除任务是 error-prone(错误倾向),Item 18 解释了 factory function(工厂函数)的 interface(接口)应该如何改变以防止普通的客户错误,但这些在这里都是次要的,因为在这个 Item 中,我们将精力集中于上面的代码中一个更基本的缺陷:即使客户做对了每一件事,也无法预知程序将如何运转。

问题在于 getTimeKeeper 返回一个 pointer to a derived class object(指向派生类对象的指针)(比如 AtomicClock),那个 object(对象)经由一个 base class pointer(基类指针)(也就是一个 TimeKeeper* pointer)被删除,而且这个 base class(基类)(TimeKeeper) 有一个 non-virtual destructor(非虚拟析构函数)。祸端就在这里,因为 C++ 规定:当一个 derived class object(派生类对象)通过使用一个 pointer to a base class with a non-virtual destructor(指向带有非虚拟析构函数的基类的指针)被删除,则结果是未定义的。运行时比较典型的后果是 derived part of the object(这个对象的派生部分)不会被析构。如果 getTimeKeeper 返回一个指向 AtomicClock object(对象)的指针,则 object(对象)的 AtomicClock 部分(也就是在 AtomicClock class 中声明的 data members(数据成员))很可能不会被析构,AtomicClock 的 destructor(析构函数)也不会运行。然而,base class part(基类部分)(也就是 TimeKeeper 部分)很可能已被析构,这就导致了一个古怪的 "partially destroyed" object(“部分被析构”对象)。这是一个导致泄漏资源,破坏数据结构以及消耗大量调试时间的绝妙方法。

消除这个问题很简单:给 base class(基类)一个 virtual destructor(虚拟析构函数)。于是,删除一个 derived class object(派生类对象)的时候就有了你所期望的行为。将析构 entire object(整个对象),包括全部的 derived class parts(派生类构件):

class TimeKeeper {
public:
  TimeKeeper();
  virtual ~TimeKeeper();
  ...
};
TimeKeeper *ptk = getTimeKeeper();
...
delete ptk;                             // now behaves correctly

类似 TimeKeeper 的 base classes(基类)一般都包含除了 destructor(析构函数)以外的其它 virtual functions(虚拟函数),因为 virtual functions(虚拟函数)的目的就是允许 derived class implementations(派生类实现)的定制化(参见 Item 34)。例如,TimeKeeper 可以有一个 virtual functions(虚拟函数)getCurrentTime,它在各种不同的 derived classes(派生类)中有不同的实现。几乎所有拥有 virtual functions(虚拟函数)的 class(类)差不多都应该有一个 virtual destructor(虚拟析构函数)。

如果一个 class(类)不包含 virtual functions(虚拟函数),这经常预示不打算将它作为 base class(基类)使用。当一个 class(类)不打算作为 base class(基类)时,将 destructor(析构函数)虚拟通常是个坏主意。考虑一个表现二维空间中的点的 class(类):

class Point {                           // a 2D point
public:
  Point(int xCoord, int yCoord);
  ~Point();
private:
  int x, y;
};

如果一个 int 占用 32 bits,一个 Point object 正好适用于 64-bit 的寄存器。而且,这样一个 Point object 可以被作为一个 64-bit 的量传递给其它语言写的函数,比如 C 或者 FORTRAN。如果 Point 的 destructor(析构函数)被虚拟,情况就完全不一样了。

virtual functions(虚拟函数)的实现要求 object(对象)携带额外的信息,这些信息用于在运行时确定该 object(对象)应该调用哪一个 virtual functions(虚拟函数)。典型情况下,这一信息具有一种被称为 vptr ("virtual table pointer")(虚拟函数表指针)的指针的形式。vptr 指向一个被称为 vtbl ("virtual table")(虚拟函数表)的 array of function pointers(函数指针数组),每一个带有 virtual functions(虚拟函数)的 class(类)都有一个相关联的 vtbl。当在一个 object(对象)上调用 virtual functions(虚拟函数)时,实际的被调用函数通过下面的步骤确定:找到 object(对象)的 vptr 指向的 vtbl,然后在 vtbl 中寻找合适的 function pointer(函数指针)。

virtual functions(虚拟函数)是如何实现的细节并不重要。重要的是如果 Point class 包含一个 virtual functions(虚拟函数),这个类型的 object(对象)的大小就会增加。在一个 32-bit 架构中,它们将从 64 bits(相当于两个 ints)长到 96 bits(两个 ints 加上 vptr);在一个 64-bit 架构中,它们可能从 64 bits 长到 128 bits,因为在这样的架构中指针的大小是 64 bits 的。为 Point 加上 vptr 将会使它的大小增长 50-100%!Point object(对象)不再适合 64-bit 寄存器。而且,Point object(对象)在 C++ 和其它语言(比如 C)中,看起来不再具有相同的结构,因为它们在其它语言中的对应物没有 vptr。结果,Points 不再可能传入其它语言写成的函数或从其中传出,除非你为 vptr 做出明确的补偿,而这是它自己的实现细节并因此失去可移植性。

最终结果就是无故地将所有 destructors(析构函数)声明为 virtual(虚拟),和从不把它们声明为 virtual(虚拟)一样是错误的。实际上,很多人总结过这条规则:declare a virtual destructor in a class if and only if that class contains at least one virtual function(当且仅当一个类中包含至少一个虚拟函数时,则在类中声明一个虚拟析构函数)。

甚至在完全没有 virtual functions(虚拟函数)时,也有可能纠缠于 non-virtual destructor(非虚拟析构函数)问题。例如,标准 string 类型不包含 virtual functions(虚拟函数),但是被误导的程序员有时将它当作 base class(基类)使用:

class SpecialString: public std::string {   // bad idea! std::string has a
  ...                                       // non-virtual destructor
};

一眼看上去,这可能无伤大雅,但是,如果在程序的某个地方因为某种原因,你将一个 pointer-to-SpecialString(指向 SpecialString 的指针)转型为一个 pointer-to-string(指向 string 的指针),然后你将 delete 施加于这个 string pointer(指针),你就立刻被放逐到 undefined behavior(未定义行为)的领地:

SpecialString *pss =   new SpecialString("Impending Doom");
std::string *ps;
...
ps = pss;                               // SpecialString* → std::string*
...
delete ps;                              // undefined! In practice,
                                        // *ps's SpecialString resources
                                        // will be leaked, because the
                                        // SpecialString destructor won't
                                        // be called.

同样的分析可以适用于任何缺少 virtual destructor(虚拟析构函数)的 class(类),包括全部的 STL container(容器)类型(例如,vector,list,set,tr1::unordered_map(参见 Item 54)等)。如果你受到从 standard container(标准容器)或任何其它带有 non-virtual destructor(非虚拟析构函数)的 class(类)继承的诱惑,一定要挺住!(不幸的是,C++ 不提供类似 Java 的 final classes(类)或 C# 的 sealed classes(类)的 derivation-prevention mechanism(防派生机制)。)

有时候,给一个 class(类)提供一个 pure virtual destructor(纯虚拟析构函数)能提供一些便利。回想一下,pure virtual functions(纯虚拟函数)导致 abstract classes(抽象类)——不能被实例化的 classes(类)(也就是说你不能创建这个类型的 objects(对象))。然而,有时候,你有一个 class(类),你希望它是抽象的,但没有任何 pure virtual functions(纯虚拟函数)。怎么办呢?因为一个 abstract classes(抽象类)注定要被用作 base class(基类),又因为一个 base class(基类)应该有一个 virtual destructor(虚拟析构函数),还因为一个 pure virtual functions(纯虚拟函数)产生一个 abstract classes(抽象类),好了,解决方案很简单:在你想要变成抽象的 class(类)中声明一个 pure virtual destructor(纯虚拟析构函数)。这是一个例子:

class AWOV {                            // AWOV = "Abstract w/o Virtuals"
public:
  virtual ~AWOV() = 0;                  // declare pure virtual destructor
};

这个 class(类)有一个 pure virtual functions(纯虚拟函数),所以它是抽象的,又因为它有一个 virtual destructor(虚拟析构函数),所以你不必担心析构函数问题。这是一个螺旋。然而,你必须为 pure virtual destructor(纯虚拟析构函数)提供一个 definition(定义):

AWOV::~AWOV() {} // definition of pure virtual dtor

destructors(析构函数)的工作方式是:most derived class(层次最低的派生类)的 destructor(析构函数)最先被调用,然后调用每一个 base class(基类)的 destructors(析构函数)。编译器会生成一个从它的 derived classes(派生类)的 destructors(析构函数)对 ~AWOV 的调用,所以你不得不确保为函数提供一个函数体。如果你不这样做,连接程序会提出抗议。

为 base classes(基类)提供 virtual destructor(虚拟析构函数)的规则仅仅适用于 polymorphic base classes(多态基类)—— base classes(基类)被设计成允许通过 base class interfaces(基类接口)对 derived class types(派生类类型)进行操作。TimeKeeper 就是一个 polymorphic base classes(多态基类),因为即使我们只有类型为 TimeKeeper 的 pointers(指针)指向它们的时候,我们也期望能够操作 AtomicClock 和 WaterClock objects(对象)。

并非所有的 base classes(基类)都被设计用于 polymorphically(多态)。例如,无论是 standard string type(标准 string 类型),还是 STL container types(STL 容器类型)全被设计成 base classes(基类),可没有哪个是 polymorphic(多态)的。一些 classes(类)虽然被设计用于 base classes(基类),但并非被设计用于 polymorphically(多态)。这样的 classes(类)——例如 Item 6 中的 Uncopyable 和标准库中的 input_iterator_tag(参见 Item 47)——没有被设计成允许经由 base class interfaces(基类接口)对 derived class objects(派生类对象)进行操作。所以它们就不需要 virtual destructor(虚拟析构函数)。

Things to Remember

  • polymorphic base classes(多态基类)应该声明 virtual destructor(虚拟析构函数)。如果一个 class(类)有任何 virtual functions(虚拟函数),它就应该有一个 virtual destructor(虚拟析构函数)。

  • 不是设计用来作为 base classes(基类)或不是设计用于 polymorphically(多态)的 classes(类)就不应该声明 virtual destructor(虚拟析构函数)。