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);
    ...
};
View Code

    那么也就允许这样的代码:

Animal* pAnimal1;
Animal* pAnimal2;
...
*pAnimal1=*pAnimal2;
View Code

    由于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);
    ...
};
View Code

    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;
}
View Code

    由于同型赋值实际上不需要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));
}
View Code

    当使用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);
    ...
};
View Code

    采用这种策略直接禁止了像*pAbstractAnimal1=*pAbstractAnimal2的操作,而仍然像*pAnimal1=*pAnimal2的操作并且在编译时检查类型.

2. 1的分析过程实际上体现了这样一种思想:当具体类被当做基类使用时,应该将具体类转变为抽象基类.

    然而有时候需要使用第三方库,并继承其中一个具体类,由于无法修改该库,也就无法将该具体类转为抽象基类,这是就需要采取其他选择:

    1). 继承自现有的具体类,但要注意1所提出的assignment问题,并小心条款3所提出的数组陷阱.

    2). 试着在继承体系中找一个更高层的抽象类,然后继承它.

    3). 以"所希望继承的那么程序库类"来实现新类.例如使用复合或private继承并提供相应接口.此策略不具灵活性.

    4). 为"所希望继承的那么程序库类"定义一些non-member,不再定义新类.

 

posted @ 2015-10-13 22:09  Reasno  阅读(568)  评论(0编辑  收藏  举报