More Effective C++ 条款33 将非尾端(non-leaf classes)设计为抽象类(abstract classes)
1. 假如程序有一个Chicken class,Lizard class,Animal class,其中Chicken class和Lizard class继承自Animal class,整个继承体系像这样:
Animal负责具体化所有东吴的共同特征,Lizard和Chicken是需要特殊对待的两种动物,它们的简化定义像这样:
class Animal { public: Animal& operator=(const Animal& rhs); ... }; class Lizard: public Animal { public: Lizard& operator=(const Lizard& rhs); ... }; class Chicken: public Animal { public: Chicken& operator=(const Chicken& rhs); ... };
那么也就允许这样的代码:
Animal* pAnimal1; Animal* pAnimal2; ... *pAnimal1=*pAnimal2;
由于Animal*具有多态性,也就是说它实际所指向的类型是不确定的,因此*pAnimal1和*pAnimal2的动态类型不同而导致operator=被错用的情况也可能出现,类的设计者有责任对这种赋值行为进行检查或避免"异型赋值"的出现.
最直接的方法是将operator=设为virtual,像这样:
class Animal { public: virtual Animal& operator=(const Animal& rhs); ... }; class Lizard: public Animal { public: virtual Lizard& operator=(const Animal& rhs); ... }; class Chicken: public Animal { public: virtual Chicken& operator=(const Animal& rhs); ... };
C++要求虚函数原型相同,尽管允许函数返回引用时可以返回派生类引用,但函数参数仍然必须完全相同.这就要求在Lizard和Chicken的operator=内部使用dynamic_cast,Lizard的operator=的定义像这样:
Lizard& Lizard::operator=(const Animal& rhs) { //将rhs转为const Lizard&,失败则抛出异常 const Lizard& rhs_liz = dynamic_cast<const Lizard&>(rhs); proceed with a normal assignment of rhs_liz to *this; }
由于同型赋值实际上不需要dynamic_cast,因此opeator=的实现可以升级成这样:
class Lizard: public Animal { public: virtual Lizard& operator=(const Animal& rhs); Lizard& operator=(const Lizard& rhs); //新增的non-virtual函数 ... }; Lizard& Lizard::operator=(const Animal& rhs) { return operator=(dynamic_cast<const Lizard&>(rhs)); }
当使用dynamic_cast有其固有的缺点:dynamic_cast可能抛出异常,也就是说编译器会产生额外代码用于"待命捕捉bad_cast exceptions,并作某些合理应对",因此会产生一定的效率损失;此外错误只有在运行期才能检查出来.因此应该考虑其他策略.
阻止类似于*pAnimal1=*pAnimal2的行为的最直接办法就是将Animal的operator=设为private,但这样一来Animal的自身赋值也不允许,而且Lizard和Chicken的operator=也无法实现,因为它们默认调用Animal的operator=,即使将Animal的operator=设为protected,前一个问题仍然存在.最简单的办法就是消除Animal对象相互赋值的需要,也就是将Animal设为抽象基类,但又由于Animal可能必须作为一个具体类,因此另一个策略就是使Lizard和Chicken不再继承自Animal,并使得Lizard,Chicken,Animal继承自一个更痛的抽象基类AbstactAnimal,整个继承体系像这样:
AbstractAnimal,Lizard,Animal,Chicken的定义像这样:
class AbstractAnimal { protected: AbstractAnimal& operator=(const AbstractAnimal& rhs); public: virtual ~AbstractAnimal() = 0; // 虽然AbstractAnimal的析构函数被设为pure virtual,但仍然需要提供定义 ... }; class Animal: public AbstractAnimal { public: Animal& operator=(const Animal& rhs); ... }; class Lizard: public AbstractAnimal { public: Lizard& operator=(const Lizard& rhs); ... } class Chicken: public AbstractAnimal { public: Chicken& operator=(const Chicken& rhs); ... };
采用这种策略直接禁止了像*pAbstractAnimal1=*pAbstractAnimal2的操作,而仍然像*pAnimal1=*pAnimal2的操作并且在编译时检查类型.
2. 1的分析过程实际上体现了这样一种思想:当具体类被当做基类使用时,应该将具体类转变为抽象基类.
然而有时候需要使用第三方库,并继承其中一个具体类,由于无法修改该库,也就无法将该具体类转为抽象基类,这是就需要采取其他选择:
1). 继承自现有的具体类,但要注意1所提出的assignment问题,并小心条款3所提出的数组陷阱.
2). 试着在继承体系中找一个更高层的抽象类,然后继承它.
3). 以"所希望继承的那么程序库类"来实现新类.例如使用复合或private继承并提供相应接口.此策略不具灵活性.
4). 为"所希望继承的那么程序库类"定义一些non-member,不再定义新类.