《More Effictive C++》学习笔记 — 杂项
条款33 — 将非尾端类设计为抽象类
假设我们正在做一个项目,以软件来处理动物。大部分动物的实现都一致,其中有两种动物需要特殊处理:蜥蜴和鸡,在此情况下,这几个类之间的关系可能是这样的:
Animal 类负责处理所有的动物的共同特征,Lizard 和 Chicken 负责两种动物的特化处理。
1、赋值操作符的切割问题
class Animal
{
public:
Animal& operator=(const Animal& rhs)
{
...
return *this;
}
};
class Lizard : public Animal
{
public:
Lizard& operator=(const Lizard& rhs)
{
...
return *this;
}
};
class Chicken : public Animal
{
public:
Chicken& operator=(const Chicken& rhs)
{
...
return *this;
}
};
考虑这样的调用代码:
Lizard lizard1;
Lizard lizard2;
Animal* animal1 = &lizard1;
Animal* animal2 = &lizard2;
*animal1 = *animal2;
很明显,这将会发生切割问题,其主要原因是 animal1 的静态类型和动态类型不一致,而编译器并不知道。
2、使用虚函数赋值
如果我们想让编译器知道它们的静态类型,可以让赋值操作符成为虚函数:
class Animal
{
public:
virtual Animal& operator=(const Animal& rhs)
{
...
return *this;
}
};
class Lizard : public Animal
{
public:
virtual Lizard& operator=(const Lizard& rhs)
{
...
return *this;
}
};
虚函数支持返回值类型协变,却不支持参数的动态转换。事实上,只要我们在派生类虚函数后面加上 override 关键字就可以很轻松地检查出来这个问题。因此,虚函数应该是下面这样:
class Animal
{
public:
virtual Animal& operator=(const Animal& rhs)
{
...
return *this;
}
};
class Lizard : public Animal
{
public:
virtual Lizard& operator=(const Animal& rhs) override
{
...
return *this;
}
};
class Chicken : public Animal
{
public:
virtual Chicken& operator=(const Animal& rhs) override
{
...
return *this;
}
};
这解决了前面的问题,但是又引入了一个新的问题:
Lizard lizard1;
Chicken chicken1;
Animal* animal1 = &lizard1;
Animal* animal2 = &chicken1;
*animal1 = *animal2;
两个操作数的类型竟然可以不同。这可不是我们想要的。我们希望不同类型的对象之间不可以相互赋值。
3、使用RTTI
借助动态类型转换,我们得以这样识别出右操作数的类型:
Lizard& Lizard::operator=(const Animal& rhs) override
{
const Lizard& lizard = dynamic_cast<const Lizard&>(rhs);
...
return *this;
}
当然,在类型不匹配的情况下此函数会抛出异常。
除此之外,我们发现,这种修改导致两个 Lizard 对象之间的赋值也要进行类型检查。这有点不好。我们考虑增加普通的赋值函数:
class Lizard : public Animal
{
public:
virtual Lizard& operator=(const Animal& rhs) override;
Lizard& operator=(const Lizard& rhs);
};
那么第一个赋值操作也可以简化为:
virtual Lizard& Lizard::operator=(const Animal& rhs) override
{
return operator=(dynamic_cast<const Lizard&>(rhs));
}
这样做对用户的要求就是他们每次调用赋值操作时都需要使用 try-catch 语句进行异常捕获。这并不合适,增加了代码的复杂度。
4、阻止赋值操作符被访问
综上来说,我们如果执意使用 operator= 进行赋值(如果愿意实现 clone 方法以拷贝自然另当别论),那么并没有很好的办法解决前面遇到的所有问题。因此,最好的选择就是阻止这种赋值动作:
class Animal
{
private:
Animal& operator=(const Animal& rhs)
{
return *this;
}
};
class Lizard : public Animal
{
public:
Lizard& operator=(const Lizard& rhs);
};
class Chicken : public Animal
{
public:
Chicken& operator=(const Chicken& rhs);
};
那么下面的行为就正如我们期望那样:
Lizard lizard1, lizard2;
lizard1 = lizard2; // valid
Chicken chicken1, chicken2;
chicken1 = chicken2; // valid
Animal* animal1 = &lizard1;
Animal* animal2 = &chicken1;
*animal1 = *animal2; // invalid
然而,这将导致 animal 对象之间的相互赋值不合法。同时,Lizard 和 Chicken 的赋值操作符的实现也变得很困难,因为它们无法调用相应的基类版本。针对第二个问题,我们可以将 Animal 的赋值操作符变为 protected。但是,第一个问题仍悬而未决。
5、消除对象相互赋值的需要
最简单的办法就是消除允许 Animal 对象相互赋值的需要,而完成此事的最简单做法就是让 Animal 成为一个抽象类。那些 Animal 的实例怎么办呢?可以提取抽象的功能到 AbstractAnimal 中,让 Animal,Lizard 和 Chicken 都继承于它:
各个类的定义如下:
class AbstractAnimal
{
protected:
AbstractAnimal& operator=(const AbstractAnimal& rhs)
{
return *this;
}
public:
virtual ~AbstractAnimal() = 0;
};
class Animal : public AbstractAnimal
{
public:
Animal& operator=(const AbstractAnimal& rhs)
{
return *this;
}
};
class Lizard : public AbstractAnimal
{
public:
Lizard& operator=(const Lizard& rhs);
};
class Chicken : public AbstractAnimal
{
public:
Chicken& operator=(const Chicken& rhs);
};
6、总结
上述对于”通过基类的指针进行赋值动作“的讨论,是以”具体基类含有成员数据“的假设作为基础。如果具体基类中没有数据成员,也许你会觉得就没问题了。其实并不是,一方面,我们要以发展的眼光看待问题。在具体类中,未来是可能出现数据的。另一方面,没有成员数据的类不就该是抽象类吗?
使用抽象基类代替具体基类,最大的好处在于强迫我们发现有用的抽象性质。如果有个具体类 C1 和 C2,而你希望 C2 以公有方式继承 C1。你应该将原本的双继承体系变为三继承体系,正如我们对 Animal 所做的。这种转变的主要价值在于,它强迫我们验明抽象类A。很显然,C1 和 C2 有某些共同的东西,所以它们之间的关系是公有继承。如果采用上述转变,我们就必须鉴定出何为共同特征并将抽取称C++类。
面向对象的设计目标是辨识处一些有用的抽象性,并强迫它们成为抽象类。