条款 39:明智而审慎地使用 private 继承

《Effective C++ 中文版 第三版》读书笔记

** 条款 39:明智而审慎地使用 private 继承 **

C++ 中 public 继承视为 is-a 关系。现在看 private 继承:

class Person{...}; 

class Student: private Person {...}; 

void eat(const Person& p); 

void study(const Student& s);

Person p; 

Student s; 

eat(p); 

eat(s); // 错误! 难道学生不是人?!

显然 private 继承不是 is-a 关系。

由 private base class 继承而来的所有成员,在 derived class 中都会成为 private 属性,纵使它们在 base class 中原本是 protected 或 public。private 继承意味着 implemented-in-term-of(根据某物实现)。

如果 class D 以 private 形式继承 class B,用意是为了采用 class B 内已经备妥的某些特性,不是因为 B 对象和 D 对象存在任何观念上的关系。private 继承纯粹只是一种实现技术(这就是为什么继承自 private base class 的每样东西在你的 class 内都是 private:因为他们都是实现细节而已)。用条款 34 的术语说,private 继承意味着只有实现部分被继承,接口部分应略去。如果 D 以 private 形式继承 B,意思是 D 对象根据 B 对象实现而得。private 继承在软件 “设计” 层面上没有意义,其意义只及于软件实现层面。

条款 38 说复合(composition)的意义也是 is-implemented-in-term-of,如何进行取舍?尽可能的使用复合,必要时才使用 private 继承:主要是当一个意欲成为 derived class 者想访问一个意欲成为 base class 者的 protected 成分,或为了重新定义 virtual 函数,还有一种激进情况是空间方面的厉害关系。

有个 Widget class,它记录每个成员函数的被调用次数。运行期周期性的审查那份信息。为了完成这项工作,我们需要设定某种定时器,使我们知道收集统计数据的时候是否到了。

为了复用既有代码,我们发现了 Timer:

class Timer { 
public: 
    explicit Timer(int tickFrequency); 
    virtual void onTick() const; 
};

每次滴答就调用某个 virtual 函数,我们可以重新定义那个 virtual 函数,让后者取出 Widget 的当时状态。

为了让 Widget 重新定义 Timer 内的 virtual 函数,Widget 必须继承自 Timer。但 public 继承并不适当,因为 Widget 并不是一个 Timer。不能够对一个 Widget 调用 onTick 吧,观念上那并不是 Wigdet 接口的一部分。

我们必须用 private 继承 Timer:

class Widget: private Timer{ 
private: 
    virtual void onTick() const; 
};

再说一次,把 onTick 放进 public 接口内会导致客户以为他们可以调用它,那就违反了条款 18.

这个设计好,但不值几文钱,private 继承并非绝对必要。如果我们决定用复合取而代之,是可以的,只要在 Widget 内声明一个嵌套式 private class,后者以 public 形式继承 Timer 并重新定义 onTick,然后放一个这种类型的对象在 Widget 内:

class Widget{ 
private: 
    class WidgetTimer: public Timer{ 
    public: 
        virtual void onTick() const; 
    }; 

    WidgetTimer timer; 
};

这个设计比只是用 private 继承复杂一些,但是有两个理由可能你愿意或应该选择这样的 public 继承加复合:

首先,或许会想设计 Widget 使它得以拥有 derived classes,但同时你可能会想阻止 derived clssses 重新定义 onTick。如果 Widget 继承自 Timer,上面的想法就不可能实现,即使是 private 继承也不可能。(条款 35 说 derived class 可以重新定义 virtual 函数,即使它们不得调用它)但如果 WidgetTimer 是 Widget 内部的一个 private 成员并继承 Timer,Widget 的 derived classes 将无法取用 WidgetTimer,因此无法继承它或重新定义它的 virtual 函数。有些类似 java 的 final 或 C# 的 sealed。

第二,或许想要将 Widget 的编译依存性降至最低,若 Widget 继承 Timer,当 Widget 被编译时,timer 的定义必须可见,所以定义 Widget 的那个文件必须包含 Timer.h。但如果 WidgetTimer 移除 Widget 之外而 Widget 内含一个指针指向 WidgetTimer,Widget 可以只带着一个简单的 WidgetTimer 声明式,不再需要 #include 任何与 timer 有关的东西。对大型系统而言,如此的解耦(decouplings)可能是重要的措施。

还有一种激进情况,只适用于你所处理的 class 不带任何数据时。这样的 classes 没有 non-static 成员变量,没有 virtual 函数(这种函数会为每个对象带来一个 vptr),也没有 virtual base classes(这样的 base classes 也会招致体积上的额外开销,见条款 40)。这种所谓的 empty class 对象不使用任何空间,因为没有任何隶属对象的数据要存储,然而由于技术上的理由,C++ 裁定凡是独立(非附属)对象都必须有非零大小:

class Empty{}; 

class HoldsAnInt { 

private: 
    int x; 
    Empty e; // 应该不需要任何内存 
};

你会发现 sizeof(HoldsAnInt) > sizeof(int);一个 Empty 成员变量竟然要求内存。在多数编译器中 sizeof(Empty) 获得 1,因为面对 “大小为零的独立对象”,通常 C++ 官方勒令默默安插一个 char 到空对象内。然而齐位需求(alignment)可能造成编译器为类似 HoldsAnInt 这样的 class 加上一些衬垫(padding),所以有可能 HoldsAnInt 对象不只多一个 char 大小,实际上放大到多一个 int。

独立(非附属)这个约束不适用于 derived class 对象内的 base class 成分,因为它们并非独立。如果你继承 Empty,而不是内含一个那种类型的对象:

class HoldsAnInt: private Empty{ 
private: 
    int x; 
};

几乎可以确定 sizeof(HoldsAnInt) == sizeof(int)。这是所谓的 EBO(empty base optimization:空白基类最优化),如果你是一个库开发成员,而你的客户非常在意空间,那么值得注意 EBO。另外一个值得知道的是,一般 EBO 只在单一继承(而非多继承)下才可行。

现实中的 “Empty” class 并不是真的 empty。虽然他们从未拥有 non-static 成员变量,却往往内含 typedefs, enums, static 成员变量,或 non-virtual 函数。stl 就有许多技术用途的 empty classes,其中内含有用的成员(通常是 typedefs),包括 base classes unary_function 和 binary_function,这些是 “用户自定义的函数对象” 通常都会继承的 classes。感谢 EBO 的广泛实践,这样的继承很少增加 derived classes 的大小。

尽管如此,大多数 class 并非 empty,所以 EBO 很少成为 private 继承的正当理由。复合和 private 继承都意味着 is-implemented-in-term-of,但复合比较容易理解,所以无论什么时候,只要可以,还是应该选择复合。

请记住:

  1. private 继承意味 is-implementation-in-terms of(根据某物实现出)。她通常比复合级别低。但是当 derived class 需要访问 protected base class 的成员,或需要重新定义继承而来的 virtual 函数时,这么设计是合理的。

  2. 和复合不同,private 继承可以造成 empty base 最优化。这对致力于 “对象尺寸最小化” 的程序库开发者而言,可能很重要。

你可能感兴趣的:(条款 39:明智而审慎地使用 private 继承)