首页 > C/C++语言 > C/C++基本语法 > C++箴言:谨慎使用私有继承
2006
02-20

C++箴言:谨慎使用私有继承

 在《C++箴言:确保公开继承模拟“is-a”》一文中论述了 C++ 将 public inheritance(公有继承)视为一个 is-a 关系。当给定一个 hierarchy(继承体系),其中有一个 class Student 从一个 class Person 公有继承,当为一个函数调用的成功而有必要时,需要将 Students 隐式转型为 Persons,它通过向编译器展示来做到这一点。用 private inheritance(私有继承)代替 public inheritance(公有继承)把这个例子的一部分重做一下是值得的:

class Person { … };
class Student: private Person { … }; // inheritance is now private

void eat(const Person& p); // anyone can eat

void study(const Student& s); // only students study

Person p; // p is a Person
Student s; // s is a Student

eat(p); // fine, p is a Person

eat(s); // error! a Student isn’t a Person

  很明显,private inheritance(私有继承)不意味着 is-a。那么它意味着什么呢?

  “喂!”你说:“在我们得到它的含义之前,我们先看看它的行为。private inheritance(私有继承)有怎样的行为呢?”好吧,支配 private inheritance(私有继承)的第一个规则你只能从动作中看到:与 public inheritance(公有继承)对照,如果 classes(类)之间的 inheritance relationship(继承关系)是 private(私有)的,编译器通常不会将一个 derived class object(派生类对象)(诸如 Student)转型为一个 base class object(基类对象)(诸如 Person)。这就是为什么为 object(对象)s 调用 eat 会失败。第二个规则是从一个 private base class(私有基类)继承的 members(成员)会成为 derived class(派生类)的 private members(私有成员),即使它们在 base class(基类)中是 protected(保护)的或 public(公有)的。

  行为不过如此。这就给我们带来了含义。private inheritance(私有继承)意味着 is-implemented-in-terms-of(是根据……实现的)。如果你使 class(类)D 从 class(类)B 私有继承,你这样做是因为你对于利用在 class(类)B 中才可用的某些特性感兴趣,而不是因为在 types(类型)B 和 types(类型)D 的 objects(对象)之间有什么概念上的关系。同样地,private inheritance(私有继承)纯粹是一种实现技术。(这也就是为什么你从一个 private base class(私有基类)继承的每一件东西都在你的 class(类)中变成 private(私有)的原因:它全部都是实现的细节。)利用《接口继承和实现继承》中提出的条款,private inheritance(私有继承)意味着只有 implementation(实现)应该被继承;interface(接口)应该被忽略。

  如果 D 从 B 私有继承,它就意味着 D objects are implemented in terms of B objects(D 对象是根据 B 对象实现的),没有更多了。private inheritance(私有继承)在 software design(软件设计)期间没有任何意义,只在 software implementation(软件实现)期间才有。 private inheritance(私有继承)意味着 is-implemented-in-terms-of(是根据……实现的)的事实有一点混乱,正如《通过composition模拟“has-a”》一文中所指出的 composition(复合)也有同样的含义。你怎么预先在它们之间做出选择呢?答案很简单:只要你能就用 composition(复合),只有在绝对必要的时候才用 private inheritance(私有继承)。什么时候是绝对必要呢?主要是当 protected members(保护成员)和/或 virtual functions(虚拟函数)掺和进来的时候,另外还有一种与空间相关的极端情况会使天平向 private inheritance(私有继承)倾斜。我们稍后再来操心这种极端情况。

  毕竟,它只是一种极端情况。 假设我们工作在一个包含 Widgets 的应用程序上,而且我们认为我们需要更好地理解 Widgets 是怎样被使用的。例如,我们不仅要知道 Widget member functions(成员函数)被调用的频度,还要知道 call ratios(调用率)随着时间的流逝如何变化。带有清晰的执行阶段的程序在不同的执行阶段可以有不同的行为侧重。例如,一个编译器在解析阶段对函数的使用与优化和代码生成阶段就有很大的不同。

  我们决定修改 Widget class 以持续跟踪每一个 member function(成员函数)被调用了多少次。在运行时,我们可以周期性地检查这一信息,与每一个 Widget 的这个值相伴的可能还有我们觉得有用的其它数据。为了进行这项工作,我们需要设立某种类型的 timer(计时器),以便在到达收集用法统计的时间时我们可以知道。

  尽可能复用已有代码,而不是写新的代码,我在我的工具包中翻箱倒柜,而且满意地找到下面这个 class(类):

class Timer {
public:
explicit Timer(int tickFrequency);
virtual void onTick() const; // automatically called for each tick

};

  这正是我们要找的:一个我们能够根据我们的需要设定 tick 频率的 Timer object,而在每次 tick 时,它调用一个 virtual function(虚拟函数)。我们可以重定义这个 virtual function(虚拟函数)以便让它检查 Widget 所在的当前状态。很完美!

  为了给 Widget 重定义 Timer 中的一个 virtual function(虚拟函数),Widget 必须从 Timer 继承。但是 public inheritance(公有继承)在这种情况下不合适。Widget is-a(是一个)Timer 不成立。Widget 的客户不应该能够在一个 Widget 上调用 onTick,因为在概念上那不是的 Widget 的 interface(接口)的一部分。允许这样的函数调用将使客户更容易误用 Widget 的 interface(接口),这是一个对《使接口易于正确使用难错误使用》中的关于“使接口易于正确使用,而难以错误使用”的建议的明显违背。public inheritance(公有继承)在这里不是正确的选项。

  因此我们就 inherit privately(秘密地继承):

class Widget: private Timer {
private:
virtual void onTick() const; // look at Widget usage data, etc.

};

  通过 private inheritance(私有继承)的能力,Timer 的 public(公有)onTick 函数在 Widget 中变成 private(私有)的,而且在我们重新声明它的时候,也把它保留在那里。重复一次,将 onTick 放入 public interface(公有接口)将误导客户认为他们可以调用它,而这违背了我在《使接口易于正确使用难错误使用》。

  这是一个很好的设计,但值得注意的是,private inheritance(私有继承)并不是绝对必要的。如果我们决定用 composition(复合)来代替,也是可以的。我们仅需要在我们从 Timer 公有继承来的 Widget 内声明一个 private nested class(私有嵌套类),在那里重定义 onTick,并在 Widget 中放置一个那个类型的 object(对象)。以下就是这个方法的概要:

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

  这个设计比只用了 private inheritance(私有继承)的那一个更复杂,因为它包括 (public) inheritance((公有)继承)和 composition(复合)两者,以及一个新 class (WidgetTimer) 的引入。老实说,我出示它主要是为了提醒你有多于一条的道路通向一个设计问题,而且它也可以锻炼你自己你自己考虑多种方法(参见《C++箴言:最小化文件之间的编译依赖》)。然而,我可以想到为什么你可能更愿意用 public inheritance(公有继承)加 composition(复合)而不用 private inheritance(私有继承)的两个原因。

  首先,你可能要做出允许 Widget 有 derived classes(派生类)的设计,但是你还可能要禁止 derived classes(派生类)重定义 onTick。如果 Widget 从 Timer 继承,那是不可能的,即使 inheritance(继承)是 private(私有)的也不行。(回忆《C++箴言:考虑可选的虚拟函数的替代方法》derived classes(派生类)可以重定义 virtual functions(虚拟函数),即使调用它们是不被允许的。)但是如果 WidgetTimer 在 Widget 中是 private(私有)的而且是从 Timer 继承的,Widget 的 derived classes(派生类)就不能访问 WidgetTimer,因此就不能从它继承或重定义它的 virtual functions(虚拟函数)。如果你曾在 Java 或 C# 中编程并且错过了禁止 derived classes(派生类)重定义 virtual functions(虚拟函数)的能力(也就是,Java 的 final methods(方法)和 C# 的 sealed),现在你有了一个在 C++ 中的到类似行为的想法。

  第二,你可能需要最小化 Widget 的 compilation dependencies(编译依赖)。如果 Widget 从 Timer 继承,在 Widget 被编译的时候 Timer 的 definition(定义)必须是可用的,所以定义 Widget 的文件可能不得不 #include Timer.h。另一方面,如果 WidgetTimer 移出 Widget 而 Widget 只包含一个指向一个 WidgetTimer 的 pointer(指针),Widget 就可以只需要 WidgetTimer class(类)的一个简单的 declaration(声明);为了使用 Timer 它不需要 #include 任何东西。对于大型系统,这样的隔离可能非常重要(关于 minimizing compilation dependencies(最小化编译依赖)的细节,参见《C++箴言:最小化文件之间的编译依赖》)。

  我早些时候谈及 private inheritance(私有继承)主要用武之地是当一个将要成为 derived class(派生类)的类需要访问将要成为 base class(基类)的类的 protected parts(保护构件),或者希望重定义一个或多个它的 virtual functions(虚拟函数),但是 classes(类)之间的概念上的关系却是 is-implemented-in-terms-of,而不是 is-a。然而,我也说过有一种涉及 space optimization(空间最优化)的极端情况可能会使你倾向于 private inheritance(私有继承),而不是 composition(复合)。

  这个极端情况确实非常尖锐:它仅仅适用于你处理一个其中没有数据的 class(类)的时候。这样的 classes(类)没有 non-static data members(非静态数据成员);没有 virtual functions(虚函数)(因为存在这样的函数会在每一个 object(对象)中增加一个 vptr ——参见《C++箴言:多态基类中将析构函数声明为虚拟》);也没有 virtual base classes(虚拟基类)(因为这样的 base classes(基类)也会引起 size overhead(大小成本))。在理论上,这样的 empty classes(空类)的 objects(对象)应该不占用空间,因为没有 per-object(逐对象)的数据需要存储。然而,由于 C++ 天生的技术上的原因,freestanding objects(独立对象)必须有 non-zero size(非零大小),所以如果你这样做,

class Empty {}; // has no data, so objects should
// use no memory
class HoldsAnInt { // should need only space for an int
private:
 int x;
 Empty e; // should require no memory
};

  你将发现 sizeof(HoldsAnInt) > sizeof(int);一个 Empty data member(空数据成员)需要存储。对以大多数编译器,sizeof(Empty) 是 1,这是因为 C++ 法则反对 zero-size 的 freestanding objects(独立对象)一般是通过在 “empty” objects(“空”对象)中插入一个 char 完成的。然而,alignment requirements(对齐需求)可能促使编译器向类似 HoldsAnInt 的 classes(类)中增加填充物,所以,很可能 HoldsAnInt objects 得到的不仅仅是一个 char 的大小,实际上它们可能会扩张到足以占据第二个 int 的位置。(在我测试过的所有编译器上,这毫无例外地发生了。)

  但是也许你已经注意到我小心翼翼地说 “freestanding” objects(“独立”对象)必然不会有 zero size。这个约束不适用于 base class parts of derived class objects(派生类对象的基类构件),因为它们不是独立的。如果你用从 Empty 继承代替包含一个此类型的 object(对象),

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

  你几乎总是会发现 sizeof(HoldsAnInt) == sizeof(int)。这个东西以 empty base optimization (EBO)(空基优化)闻名,而且它已经被我测试过的所有编译器实现。如果你是一个空间敏感的客户的库开发者,EBO 就值得了解。同样值得了解的是 EBO 通常只在 single inheritance(单继承)下才可行。支配 C++ object layout(C++ 对象布局)的规则通常意味着 EBO 不适用于拥有多于一个 base(基)的 derived classes(派生类)。

  在实践中,”empty” classes(“空”类)并不真的为空。虽然他们绝对不会有 non-static data members(非静态数据成员),但它们经常会包含 typedefs,enums(枚举),static data members(静态数据成员),或 non-virtual functions(非虚拟函数)。STL 有很多包含有用的 members(成员)(通常是 typedefs)的专门的 empty classes(空类),包括 base classes(基类)unary_function 和 binary_function,user-defined function objects(用户定义函数对象)通常从这些 classes(类)继承而来。感谢 EBO 的普遍实现,这样的继承很少增加 inheriting classes(继承来的类)的大小。

  尽管如此,我们还是要回归基础。大多数 classes(类)不是空的,所以 EBO 很少会成为 private inheritance(私有继承)的一个合理的理由。此外,大多数 inheritance(继承)相当于 is-a,而这正是 public inheritance(公有继承)而非 private(私有)所做的事。composition(复合)和 private inheritance(私有继承)两者都意味着 is-implemented-in-terms-of(是根据……实现的),但是 composition(复合)更易于理解,所以你应该尽你所能使用它。

  private inheritance(私有继承)更可能在以下情况中成为一种设计策略,当你要处理的两个 classes(类)不具有 is-a(是一个)的关系,而且其中的一个还需要访问另一个的 protected members(保护成员)或需要重定义一个或更多个它的 virtual functions(虚拟函数)。甚至在这种情况下,我们也看到 public inheritance 和 containment 的混合使用通常也能产生你想要的行为,虽然有更大的设计复杂度。谨慎使用 private inheritance(私有继承)意味着在使用它的时候,已经考虑过所有的可选方案,只有它才是你的软件中明确表示两个 classes(类)之间关系的最佳方法。

  Things to Remember

  ·private inheritance(私有继承)意味着 is-implemented-in-terms of(是根据……实现的)。它通常比 composition(复合)更低级,但当一个 derived class(派生类)需要访问 protected base class members(保护基类成员)或需要重定义 inherited virtual functions(继承来的虚拟函数)时它就是合理的。

  ·与 composition(复合)不同,private inheritance(私有继承)能使 empty base optimization(空基优化)有效。这对于致力于最小化 object sizes(对象大小)的库开发者来说可能是很重要的。


留下一个回复