第六章,杂项讨论

33-将非尾端类(non-leaf classes)设计为抽象类

继承具体类导致的”赋值(切片)与异型赋值问题:”

以下,liz1只有Animal成分被修改和liz2相同

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


Lizard liz1;
Lizard liz2;

Animal *pAnimal1 = &liz1;
Animal *pAnimal2 = &liz2;

...
*pAnimal1 = *pAnimal2;

解决办法1: 将assignment成为虚函数
如果Animal::operator=是虚函数,之前代码调用的就会是Lizard的assignment操作符

class Animal{
public:
virtual Animal& operator=(const Animal& rgs);
...
};

class Lizard: public Animal{
public:
virtual Lizard& operator=(const Animal& rhs);
};

class Chicken: public Animal{
public:
virtual Chicken& operator=(const Animal& rhs);
};

/*虽然可以调用对应的operator=函数了,
但是这样的设计将强迫我们在每一个class中为此虚函数声明完全相同的参数类型。
这意味着Lizard和Chicken classes的assignment操作符必须接受“任何种类Animal对象出现在赋值动作右边”。
*/

//将assignment操作符变为虚函数带来的问题: “异型赋值”
Lizard liz;
Chicken chick;

Animal *pAnimal1 = &liz;
Animal *pAnimal2 = &chicken;
...
*pAnimal1 = *pAnimal2; //将一只鸡赋值给一只蜥蜴
//异型赋值,一旦将assignment操作符变为虚函数,便给“异型赋值”打开了一扇门

//针对“异型赋值”进一步优化
Lizard& Lizard::operator=(const Animal& rhs)
{
//确定rhs真的是一只蜥蜴
const Lizard& rhs_liz = dynamic_cast<const Lizard&>(rhs);//rhs实际类型和要转换的目标类型const Lizard&不符就会报错

proceed with a normal assignment of rhs_liz to *this;
}

//为什么使用const Lizard& rhs_liz = dynamic_cast<const Lizard&>(rhs);就可以确定是一只蜥蜴呢?

Lizard liz1;
Chicken chicken;

//成功!
Animal& pAnimal1 = lizard;
Lizard lizard_animal1 = dynamic_cast<Lizard&>(pAnimal1);

//错误!
Animal& pAnimal2 = chicken;
Lizard lizard_animal2 = dynamic_cast<Lizard&>(pAnimal2);

/*
dynamic_cast 之所以能够成功判断类型,是通过对象虚函数表中记录的对象类型信息条目(即 type_info )。
在 Animal& pAnimal2 = chicken; 的过程中无构造、无类型转换,只是别名绑定,因此:
1. vptr 保持不变 — 仍指向 Chicken::vtable
2. type_info 未被修改 — 仍记录 "Chicken" 类型
3. 对象内存布局完整保留 — 包括派生类特有的数据成员

正因别名机制不改变对象的底层类型信息, dynamic_cast 在执行时能够通过 vptr 查到真实的 type_info ,
进而比对目标类型与对象实际类型是否匹配(包括检查继承关系),最终准确判断转型是否合法。若匹配则成功转型,
不匹配则抛出 std::bad_cast 异常(引用版本)或返回 nullptr (指针版本),从而阻止"将鸡当成蜥蜴"的非法操作。
*/

//再进一步优化:由于使用dynamic_cast,需要咨询type_info结构,且是对于任意Animal体系下的类型,但如果类型本身就是Lizard,使用dynamic_cast会增加复杂度和成本;为了降低dynamic_cast的使用频率,再给Lizard加上传统的assignment操作符处理

class Lizard: public Animal{
public:
virtual Lizard& operator=(const Animal& rhs);
Lizard& operator=(const Lizard& rhs);//添加这行
...
};

Lizard liz1, liz2;
...
liz1=liz2; //调用“接受一个const Lizard&”的operator=

Animal *pAnimal1 = &liz1;
Animal *pAnimal2 = &liz2;
...
*pAnimal1=*pAnimal2; //调用“接受一个const Animal&”的operator=

//有了第二个operator=之后,第一个operator=的实现亦得化简为:

Lizrd& Lizard::operator=(const Animal& rhs)
{
return operator=(dynamic_cast<const Lizard&>(rhs));//这样就确保只对Animal类型进行类型检查,降低成本损耗
}

解决办法2: 将assignment成为Animal的private函数

//通过将assigment变为Animal的private函数,这样蜥蜴就只能被赋值给蜥蜴,小鸡只能被赋值给小鸡,部分赋值和异型赋值也就得以禁止

class Animal {
private:
Animal& operator=(const Animal& rhs); // 此函数如今成为 private。
...
};

class Lizard: public Animal {
public:
Lizard& operator=(const Lizard& rhs);
...
};

class Chicken: public Animal {
public:
Chicken& operator=(const Chicken& rhs);
...
};

Lizard liz1, liz2;
...
liz1 = liz2; // 很好。

Chicken chick1, chick2;
...
chick1 = chick2; // 也很好。

Animal *pAnimal1 = &liz1;
Animal *pAnimal2 = &chick1;
...
*pAnimal1 = *pAnimal2; // 错误! 企图调用 private Animal::operator=

//当然,这样的设计问题接踵而至,因为Animal是一个具体类型,这将使得Animal对象之间不能赋值
Animal animal1, animal2;
...
animal1 = animal2; //错误! 企图调用 private Animal::operator=

//并且这也妨碍了Lizard和Chicken的赋值操作,因为派生类有义务调用基类的assignment操作符

Lizard& Lizard::operator=(const Lizard& rhs)
{
if (this == &rhs) return *this;

Animal::operator=(rhs); // 错误! 企图调用 private 函数。但
// Lizard::operator= 确实必须调用该函数,
// 才能将 "Animal 成分" 赋值给 *this。
...
}


//你也许会说,那就让operator=成为protected,这样派生类可以调用基类的operator=,但是作为具体类的Animal对象之间仍然不能进行赋值操作,还有什么办法呢?

/*最简单的办法就是消除"允许Animal对象相互赋值"的需要,而完成此事的最简单做法就是让Animal成为一个抽象类。身为抽象类,Animal就无法被实例化,也就没有必要支持Animal对象之间的赋值操作了。但是如果Animal的对象是必要的呢?
解决办法就是恢复Animal作为具体类,单独创建一个新的虚类AbstractAnimal,让原来的Animal、Lizard、Chicken都继承它
*/

class AbstractAnimal{
protected:
AbstractAnimal& operator=(const AbstractAnimal& rhs);

public:
virtual ~AbstractAnimal() = 0; //稍后说明
...
};

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

/*这个设计满足了每个派生类都可以无赋值切片、无异型赋值的赋值动作,Animal具体类的对象也能进行赋值操作,并且派生类对象可以调用基类的operator=操作,解决了之前设计产生的问题。
为了将AbstractAnimal设计为抽象类,让其析构函数成为纯虚函数,但同时也提供其实现,重点在于我们得到了我们想要的抽象类(必须包含一个纯虚函数),必须实现这个纯虚函数的另一个理由是,所有继承的派生类都会调用它,所以它背心被实现。
*/

讨论总结

通过上面的例子,我们可以看到,当一个具体类作为基类,会导致派生类的设计产生出切片问题,异型赋值问题;而定义一个抽象基类就可以完美解决错误设计到来的问题,这也提醒了我们,基类的角色应该是一个不包含成员变量的抽象类,且它的存在就是一是为了支持派生类可以调用基类的operator=,二就是不会导致派生类对象之间异型赋值,切片赋值,确保一个派生类对象只能与相同类型的对象相互赋值。

如何设计继承体系

当你有两个具体类C1,C2,如果希望C2以public方式继承C1,合理的设计应该是单独创建一个抽象类A,然后让C1和C2都继承A; 而一开始C2继承C1的初衷在于他们之间有某些共同的东西,因此要采用上述合理设计就应该找出这些“共同的东西”是什么,然后将其形式化为抽象基类class A,并定义完好的member function和语义

返例:需要C2继承C1时,先定义C2的抽象类版本和的具体类版本,然后让具体类都继承抽象类。不要这样做的原因在于,这会导致太多的classes,提高维护难度;面向对象设计的目标是辨识出一些有用的抽象性,并强迫他们成为抽象类。这里的抽象性的一个判别方法就是在多个环境下都需要的性质。

何时需要定义抽象类

在设计一个类时,不应该马上就同时设计其抽象类,只有当所有概念明白后,且有必要让一个类继承另一个类时才定义抽象类进行继承;也就是说,当继承关系出现时,就意味着需要一个抽象类!

继承程序库中(无法修改)的具体类时,又该如何?

无法修改程序库以安插一个新的抽象类,所以选择有限:
1、将你的具体类继承程序库的具体类,但是要验证assignment问题(切片、异型)
2、找到程序库继承体系中更高层的抽象类,其中势必有你需要的大部分功能
3、实例化这个程序库具体类对象作为你的类成员,但这意味着程序库更新时你需要验证并更新使用它的类
4、在non-member functions中使用这些程序库具体类。功能虽然实现了,但是不好维护,效率也不够好。