问题实例
假设我们正在抽象动物,其中晰蜴和小鸡需要特别处理,当前继承体系如下:
动物类处理所有动物共有的特性,晰蜴类和小鸡类特别化动物类以适用这两种动物的特有行为。它们的简化版定义如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class 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);
...
};
考虑如下表达式:1
2
3
4
5
6Lizard liz1;
Lizard liz2;
Animal *pAnimal1 = &liz1;
Animal *pAnimal2 = &liz2;
...
*pAnimal1 = *pAnimal2;
显然,最后的赋值操作符调用的是Animal的,所以直接导致了liz1只有animal部分被修改,也就是部分赋值。
解决方案
虚函数
一个解决方法是将赋值运算符声明为虚函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class 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);
...
};
这种写法直接导致了operator=可以接受任意类型的Animal对象,也就是说,下面的代码是合法的:1
2
3
4
5Lizard liz;
Chicken chick;
Animal *pAnimal1 = &liz;
Animal *pAnimal2 = &chick;
*pAnimal1 = *pAnimal2;
显然这是一个混合类型赋值。在未引入虚函数之前,混合类型赋值会被C++的强类型原则判定非法,但引入虚函数之后它们变为了合法。
动态转换与同类型赋值
我们真正想要通过指针来完成同类型赋值,而非通过指针完成混合类型赋值,于是我们可以使用动态转换完成这一操作:1
2
3
4
5Lizard& Lizard::operator=(const Animal& rhs){
//make sure rhs is really a lizard
const Lizard& rhs_liz = dynamic_cast<const Lizard&>(rhs);
proceed with a normal assignment of rhs_liz to *this;
}
在上述代码中,如果可以转换则顺利执行operator=,否则抛出bad_cast的异常。
重载赋值
其实我们并不需要非得在运行期使用虚函数与dynamic_cast,下面的代码规避了在常规情况下它们的成本,而仅仅只需要增加了一个普通形式的赋值操作:1
2
3
4
5
6class Lizard: public Animal {
public:
virtual Lizard& operator=(const Animal& rhs);
Lizard& operator=(const Lizard& rhs);
...
};
事实上,有了第二个operator=后,第一个虚函数版operator=亦得到了简化:1
2
3Lizard& Lizard::operator=(const Animal& rhs){
return operator=(dynamic<const Lizard&>(rhs));
}
现在这个函数试图将rhs转换为一个Lizard。如果转换成功,通常的赋值操作被调用;否则,一个bad_cast异常被抛出。
私有化operator=
在看到了将operator=设置为虚函数之后的弊端后,我们应该想到重新整理代码以确保用户无法写出有问题的赋值语句,最好能够在编译期就会被拒绝。比较容易的方法是在Animal类中把operator=设为private:1
2
3
4
5
6
7
8
9
10
11
12class Animal {
private:
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本身是一个具象类,也就是说这样一来Animal对象就无法执行opertaor=操作,另外,Lizard与Chicken的赋值也不可能正确完成,因为派生类的赋值操作函数需要调用基类的赋值操作函数。第二个问题可以通过将Animal中的operator=设为protected实现,但“不允许混合类型赋值”的任务并未完成,因为Lizard与Chicken仍然可以通过Animal的指针来互相赋值。
修改继承体系
最简单的方法就是修改继承体系。由于我们之前认定Animal必须作为一个实体类存在且有其意义,因此我们创建一个新类 AbstractAnimal,来包含 Animal、 Lizard、Chikcen的共有属性,并把它设为抽象类, 各具象类均由该抽象类派生而出,修改后的继承体系如下所示:
类的定义如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class 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);...
};
至此,一切工作均已完成。同类型间的赋值被允许,部分赋值或不同类型间的赋值被禁止;派生类的赋值操作函数可以调用基类的赋值操作函数。
纯虚析构函数
当我们找不到一个函数可以作为纯虚函数时,我们定义可以析构函数为纯虚函数,把析构函数设置为纯虚函数时需要记住必须在类的定义之外实现它。
声明一个函数为纯虚函数并不意味着它没有实现,它意味着:
- 当前类是抽象类
- 任何从此类派生的实体类必须将此函数申明为一个“普通”的虚函数(不能带“= 0”)
尽管大部分纯虚函数都没有实现,但纯虚函数是一个特例,他必须被实现。因为它们在派生类析构函数被调用时也将被调用。而且,它们经常执行有用的任务,诸如释放资源或记录消息。
非尾端类与抽象类
用如AbstractAnimal这样的抽象基类替换如Animal这样的实体基类,其好处并不只有保证operator=的行为正常,它同时减少了你试图对数组使用多态的可能(More Effective C++ 3)。但该技巧最大的好处发生在设计层,因为我们强迫认定所有实体都具备明确行为准则,仅有抽象类承担了抽象的义务。
我们并没有方法明确判断未来情况是否需要抽象类,但我们可以肯定,如果存在两个具象类需要以公有继承的方式相互联系,那么通常表示当前情况需要一个抽象类:
显然,C1和C2具有共性,我们通过建立抽象类将它们的共性抽取出来,使其具备明确的语义。但实际使用中我们可能会发现自己没有权限取更改类库。
总结
非尾端类应该是抽象类。我们对类库中的程序可能没有办法,但对于能控制的代码,遵守这一约定可以提高程序的可靠性、健壮性、可读性、可扩展性。