条款 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 最优化。这对致力于 “对象尺寸最小化” 的程序库开发者而言,可能很重要。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容