32:确定你的public继承构造出is-a关系

  • public继承的意思是,子类是一种特殊的父类(is-a关系)
    • 子类必须涵盖父类每一个特点,必须无条件继承父类所有特性和接口
    • 否则没有is-a关系,不应该使用public继承
      • 因为很多时候凭生活经验判断,可能会错误判断为具有is-a关系,但是子类可能没有父类的某个特性
  • 程序设计没有银弹

33:避免覆盖继承而来的名称

  • 背景:父类中有多个重载的虚函数(同名),子类只重写了其中一个,会导致子类中父类的其他重载函数不可见
    • 根本原因:如果子类重写了父类的重载函数的一部分,在进行名字查找中,可以在相应的静态类型(子类)中查找到名字,但是类型无法匹配
  • 避免方法:
    • 对于父类的重载方法,子类要么全部重写,要么一个都不重写
      • 使用using声明
    • 使用转交函数(forwarding function)?

34:区分接口继承与实现继承

  • public继承可以分为函数接口继承函数实现继承
    • 基类中声明纯虚函数,派生类只继承其接口,且派生类需要提供实现
      • 从代码层面提醒派生类主动实现其接口,即使纯虚函数在基类中也可以有实现(派生类也需要显式指明需要使用基类中的实现)
    • 基类中声明虚函数,派生类继承其接口和缺省实现
    • 基类中声明普通函数,派生类继承其接口和实现(好的编程习惯是不对子类方法进行重写)

35:考虑virtual函数以外的其他选择

  • 通常面向多态的做法:
    • 将接口设置为virtual的
  • 通过Non-Virtual Interface(NVI)来实现template method模式
    • 将接口Func的真正实现函数onFunc设置为private virtual的
      • 基类中的private virtual方法,通过public继承到派生类,派生类可以进行重写
    • 将接口Func设置为public non-virtual的,在Func中调用onFunc
      • non-virtual的接口Func就称为virtual onFunc的wrapper
    • 优点:在接口Func中调用onFunc前后,可以前置和后置的工作
    • 缺点:在某些场景的继承体系中,virtual函数必须调用基类的版本,因此virtual函数必须是protected甚至public的,此时无法使用NVI
  • strategy模式
    • 基于Function Pointers的strategy模式
      • 直接在构造函数中传入一个函数指针,用于实现多态
      • 进一步的,可以基于C++11的std::function来实现strategy模式,在构造函数中传入一个可调用对象
      • 古典的strategy模式:将函数指针替换为类指针,使用该类中的成员函数
    • 优点:同一种类型可以使用不同的方法进行计算,而且可以在运行期变更使用的函数
    • 缺点:函数指针只能访问public成员,否则只能弱化封装性,将外部函数声明为友元

36:绝不重写继承而来的non-virtual函数

  • 从语法上看
    • 虚函数执行的是动态绑定,非虚函数执行的是静态绑定
    • 如果有多态调用的需求,设置为虚函数
  • 从设计上看
    • public继承意味着一种is-a关系,子类是一种特殊的父类,不变性(父类的共性)凌驾于特异性(子类的个性)之上
    • 重写public继承而来的non-virtual表示子类修改了父类的特性,违背了is-a关系,造成了设计上的矛盾

37:绝不重写继承而来的(虚函数的)缺省参数值

  • 虚函数执行的是动态绑定,但是缺省参数值是静态绑定
    • 因此可能执行的是动态类型版本的虚函数,但是缺省参数值是静态类型版本虚函数的,没有使用动态类型版本的缺省参数值,极易引起误会
    • 缺省参数值采用静态绑定是为了提高运行时效率,这样可以在编译期将参数确定,而非得到运行时
  • 解决方法:
    • 如果使用虚函数,则采用相同的缺省参数值
    • 使用Non-Virtual Interface(NVI)代替虚函数
      • 将接口Func设置为public non-virtual的(因此不期望被重写),并带有缺省参数,因此不管怎么继承,缺省参数值都是相同的
      • 将接口Func的实现逻辑onFunc设置为private virtual的,Func中将缺省参数传递给onFunc,调用动态版本的虚函数

38:通过复合构造出has-a关系或“根据某物实现出”

  • 复合:一个类作为另一个类的数据成员
  • 当复合发生在应用域内的对象之间时,表现出has-a的关系
    • 比如Person类中有一个Address类
  • 当复合发生在实现域内的对象之间时,表现出“根据某物实现出”的关系
    • 比如使用List类模拟实现出一个Set类

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

  • private继承的特点:
    • 如果派生类private继承自基类,则从派生类无法转换到基类
    • 但是如果派生类public继承自基类,则派生类可以slice(切掉)转换为基类
  • private继承的意义:“根据某物实现出”
    • 仅仅是为了让派生类使用基类中的某些方法,派生类与基类没有直接意义上的联系
  • private继承的使用:当需要进行“根据某物实现出”的时候
    • 能用复合,就不要用private:绝大多数private继承的场合都可以使用“public继承+复合”进行代替
      • 使用private继承:
        • 比如想在Widget的派生类中,不定义OnTick方法,即使使用private继承,在Widget的派生类中仍然可以重新定义OnTick方法(类似NVI中方法)
        • 同时Widget编译时必须依赖Timer
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        
        // 使用private继承
        class Timer{
            public:
                virtual void OnTick() const;
        };
        class Widget: private Timer{
            private:
                virtual void OnTick() const; // override
        }
        // Widget的派生类中仍有OnTick方法
        
      • 使用复合:
        • 在Widget的派生类中,可以没有OnTick方法(同C++11对成员函数使用final
        • 可以将WidgetTimer定义移出Widget,从而Widget编译时不需要Timer
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        
        class Timer{
            public:
                virtual void OnTick() const;
        };
        class Widget{
            private:
                class WidgetTimer: public Timer{
                    public:
                        virtual void OnTick() const;
                };
                WidgetTimer timer;
        };
        
    • 使用private继承的情况:空白基类最优化(Empty Base Optimization,EBO)

40: 明智而审慎地使用多重继承

  • 多重继承中可能遇到歧义调用,需要指明调用哪个基类中的接口
    • 即使同名接口一个在基类中是public的,一个是private的(不会被调用),也会发生歧义
    • 因为C++首先会找到最佳匹配函数,之后才会验证其可用性,如果两个同名的函数匹配程度相同,则发生二义性
  • 遇到菱形继承时,使用虚继承,且尽量少的在虚基类中携带数据
  • 多重继承的使用场景:public继承自某个抽象基类,private继承自某个协助实现的基类