Effective C++ 学习笔记 条款39 明智而审慎地使用private继承
条款32 展示了 C 将 public 继承视为 is-a 关系。它通过这样一个例子来说明当 Student 以 public 方式继承自 Person 时如果函数调用需要编译器会隐式地将 Student 转换为 Person。这里值得用 private 继承代替 public 继承再把那部分例子重复一遍显然private 继承并不意味着 is-a。那它意味着什么呢“等等”你说“在探讨含义之前先说说行为吧。private 继承的行为是怎样的”好的关于 private 继承的第一条规则你刚才已经见识过了与 public 继承相反如果类之间的继承关系是 private 的编译器通常不会将派生类对象如 Student转换为基类对象如 Person。这就是为什么对 s 调用 eat 会失败。第二条规则是从 private 基类继承来的成员在派生类中都会变成 private 成员即使它们在基类中是 protected 或 public 的。行为说完了。接下来就是含义。private 继承意味着 is-implemented-in-terms-of根据某物实现。如果你让类 D 私有继承自类 B你这样做是因为你感兴趣的是利用类 B 中已有的某些特性而不是因为 B 类型和 D 类型对象之间存在任何概念上的关系。因此private 继承纯粹是一种实现技术这就是为什么你从 private 基类继承的所有东西在类中都变成 private 的原因——这些都只是实现细节。使用条款34中引入的术语来说private 继承意味着只应继承实现接口应被忽略。如果 D 私有继承自 B意味着 D 对象是根据 B 对象来实现的仅此而已。private 继承在软件设计层面毫无意义只存在于软件实现层面。private 继承意味着 is-implemented-in-terms-of这个事实有点令人不安因为条款38指出组合也可以表达同样的意思。那该如何在它们之间做出选择呢答案很简单能用组合就用组合只有在必须的时候才用 private 继承。什么时候必须主要是在涉及 protected 成员和/或虚函数的时候不过还有一种边缘情况——空间方面的考量可能会让天平倾向 private 继承。边缘情况我们稍后再谈。毕竟它只是边缘情况。假设我们正在开发一个涉及 Widget 的应用程序并且决定需要更好地了解 Widget 的使用情况。例如我们不仅想知道 Widget 成员函数被调用的频率还想知道调用比率随时间如何变化。执行阶段不同的程序在不同阶段会有不同的行为特征。例如编译器在解析阶段使用的函数与优化和代码生成阶段使用的函数就有很大不同。我们决定修改 Widget 类让它能记录每个成员函数被调用的次数。在运行时我们会定期检查这些信息可能还会结合每个 Widget 的值以及我们认为有用的其他数据。为了实现这个功能我们需要某种定时器以便知道何时该收集使用统计数据。与其从头编写新代码我们更倾向于复用现有代码。于是我们在自己的工具包中翻找了一番欣喜地发现了下面这个类这正好是我们需要的。Timer 对象可以按我们需要的频率来滴答计时每次滴答时它都会调用一个虚函数。我们可以重新定义那个虚函数让它检查当前 Widget 世界的状态。完美为了让 Widget 能够重新定义 Timer 中的虚函数Widget 必须继承自 Timer。但在这种情况下public 继承并不合适。并不是说一个 Widget 是一个 Timer。Widget 的客户不应该能在 Widget 上调用 onTick因为这并不是 Widget 概念接口的一部分。允许这样的函数调用会让客户很容易错误地使用 Widget 接口这明显违反了条款18中“设计易于正确使用、难以错误使用的接口”的建议。因此public 继承在这里不是一个可行的选项。于是我们采用 private 继承通过 private 继承Timer 中的公有 onTick 函数在 Widget 中变成了私有成员而我们在重新声明它时也将其保持在私有区域。再次强调把 onTick 放在公有接口中会误导客户让他们以为自己可以调用它那就会违反条款18。这是一个不错的设计但值得指出的是private 继承并不是严格必要的。如果我们执意要用组合来代替也是可行的。我们只需在 Widget 内部声明一个私有的嵌套类让它公有继承 Timer在那里重新定义 onTick然后在 Widget 中放置一个该类型的对象即可。下面是这种方式的粗略框架这种设计比单纯使用 private 继承要复杂因为它同时涉及public继承和组合还引入了一个新类WidgetTimer。坦白说我展示这个例子主要是为了提醒你设计问题的解决方式往往不止一种训练自己考虑多种方案是很有价值的另见条款35。尽管如此我能想到两个理由让你可能更倾向于采用“public 继承加组合”的方式而非 private 继承。第一你可能想让 Widget 允许有派生类但同时又想阻止派生类重新定义 onTick。如果 Widget 直接继承自 Timer这一点就做不到——即使采用 private 继承也不行回想条款35派生类可以重新定义虚函数即使它们不被允许调用这些函数。但如果 WidgetTimer 是 Widget 中的私有成员且它继承自 Timer那么 Widget 的派生类就无法访问 WidgetTimer也就不能继承自它或重新定义它的虚函数。如果你用过 Java 或 C#并且怀念那种能够阻止派生类重新定义虚函数的能力即 Java 的 final 方法和 C# 的 sealed 方法那么现在你就有思路在 C 中近似实现那种行为了。第二你可能想最小化 Widget 的编译依赖。如果 Widget 继承自 Timer那么在编译 Widget 时 Timer 的定义必须可见所以定义 Widget 的文件可能必须 #include Timer.h。另一方面如果把 WidgetTimer 移出 Widget且 Widget 只包含一个指向 WidgetTimer 的指针那么 Widget 只需一个 WidgetTimer 类的前置声明就能应付过去无需 #include 任何与 Timer 相关的东西。对于大型系统这种解耦可能非常重要关于最小化编译依赖的细节请参见条款31。我之前说过private 继承主要在有派生类想要访问基类的 protected 成员或者想要重新定义基类的一个或多个虚函数时才有用但类之间的概念关系是 is-implemented-in-terms-of 而非 is-a。不过我也提到过还有涉及空间优化的边缘情况可能会让你倾向于 private 继承而非组合。这个边缘情况确实比较“边缘”它仅在你处理一个没有数据成员的类时才适用。这样的类没有非静态数据成员没有虚函数因为虚函数的存在会给每个对象增加一个虚表指针——参见条款7也没有虚基类因为虚基类也会带来大小开销——参见条款40。从概念上讲这种空类的对象应该不占用空间因为没有任何需要存储的、与对象相关的数据。然而由于技术上的原因C 规定独立对象freestanding objects的大小必须非零所以如果你这样做你会发现sizeof(HoldsAnInt) sizeof(int);一个 Empty 数据成员确实需要内存。在大多数编译器下sizeof(Empty) 是 1因为 C 对于零大小独立对象的禁令通常是通过在“空”对象中静默插入一个 char 来满足的。然而对齐要求参见条款50可能会让编译器给 HoldsAnInt 这样的类添加填充字节所以 HoldsAnInt 对象很可能不仅仅增加一个 char 的大小而是实际上扩大到足以容纳第二个 int在我测试过的所有编译器上情况确实如此。不过你可能已经注意到了我一直谨慎地说“独立”对象的大小不能为零。这个限制并不适用于派生类对象中的基类子对象因为它们不是独立的。如果你让 HoldsAnInt 继承自 Empty而不是包含一个该类型的对象你几乎可以肯定 sizeof(HoldsAnInt) sizeof(int)。这就是所谓的空基类优化empty base optimizationEBO在我测试过的所有编译器上都有实现。如果你是库开发者且客户关心空间EBO 值得了解。同样值得注意的是EBO 通常仅在单继承下可行因为 C 对象布局规则意味着 EBO 通常不能应用于有多个基类的派生类。在实际中“空”类并不真正是空的。虽然它们从不拥有非静态数据成员但它们通常包含 typedef、枚举、静态数据成员或非虚函数。STL 中有许多技术上为空的类却包含有用的成员通常是 typedef其中包括 unary_function 和 binary_function 这些基类——用户自定义的函数对象类通常从它们继承。得益于 EBO 的广泛实现这种继承很少会增加继承类的大小。不过还是让我们回到基本面上。大多数类都不是空的所以 EBO 很少能成为采用 private 继承的合理理由。而且大多数继承对应的是 is-a 关系那是 public 继承的职责而非 private 继承。组合和 private 继承都表示 is-implemented-in-terms-of但组合更容易理解所以只要能用组合你就应该用。private 继承最可能成为合理设计策略的场景是处理两个没有 is-a 关系的类且其中一个需要访问另一个的 protected 成员或者需要重新定义另一个的一个或多个虚函数。即便在这种情况下我们也看到 public 继承加包含containment的组合往往能实现你想要的行为尽管设计复杂度会更高。谨慎地使用 private 继承意味着在考虑过所有替代方案之后它仍然是在你的软件中表达两个类之间关系的最佳方式时才使用它。切记1.private 继承表示 is-implemented-in-terms-of。它通常不如组合但在派生类需要访问基类 protected 成员或需要重新定义继承来的虚函数时它是有意义的。2.与组合不同private 继承可以启用空基类优化。这对致力于最小化对象大小的库开发者来说可能很重要。

相关新闻