虚拟继承
《c++ primer 3th》p813
在缺省情况下,C++中的继承是按值组合的一种特殊情况。当我们写:
class Bear : public ZooAnimal { ... };
每个Bear 类对象都含有其ZooAnimal 基类子对象的所有非静态数据成员,以及在Bear中声明的非静态数据成员。类似地,当派生类自己也作为一个基类对象时,如:
class PolarBear : public Bear { ... };
则PolarBear 类对象含有在PolarBear 中声明的所有非静态数据成员,以及其Bear 子对象的所有非静态数据成员和ZooAnimal 子对象的所有非静态数据成员。
在单继承下,这种由继承支持的、特殊形式的按值组合提供了最有效的、最紧凑的对象表示。在多继承下,当一个基类在派生层次中出现多次时就会有问题。最主要的实际例子是iostream 类层次结构,ostream 和istream 类都从抽象ios 基类派生而来,而iostream 类又是从ostream 和istream 派生:
class iostream :public istream, public ostream { ... };//非虚拟继承
缺省情况下,每个iostream 类对象含有两个ios 子对象:在istream 子对象中的实例以及在ostream 子对象中的实例。这为什么不好?从效率上而言,存储ios 子对象的两个复本,浪费了存储区,因为iostream 只需要一个实例。而且ios 构造函数被调用了两次,每个子对象一次。更严重的问题是由于两个实例引起的二义性。例如,任何未限定修饰地访问ios 的成员都将导致编译时刻错误:到底访问哪个实例?如果ostream 和istream 对其ios 子对象的初始化稍稍不同,会怎样呢?怎样通过iostream 类保证这一对ios 值的一致性?在缺省的按值组合机制下,真的没有好办法可以保证这一点。
C++语言的解决方案是,提供另一种可替代“按引用组合”的继承机制:虚拟继承(virtual Inheritance)。 在虚拟继承下,只有一个共享的基类子对象被继承,而无论该基类在派生层次中出现多少次。共享的基类子对象被称为虚拟基类(virtual base class)。在虚拟继承下,基类子对象的复制及由此而引起的二义性都被消除了。
为了讨论虚拟继承的语法和语义,我们选择用Panda 类作为教学示例。在动物学领域中,人们对Panda属于浣熊科(Raccoon )还是熊(Bear) 科,已经激烈争论了100 多年。由于软件设计主要是一种服务性工业,所以,我们最实际的解决方案是同时从两者派生:
class Panda : public Bear,public Raccoon, public Endangered { ... };
虚拟继承Panda 层次结构如下图所示,其中两个虚箭头分别表示Bear 和Raccoon 从ZooAnimal 的虚拟派生,而三个实箭头分别表示Panda 从Bear、Raccoon 和Endangered的非虚拟派生:
如果仔细查看上图,我们会注意到虚拟继承的不直观部分:虚拟派生(本例中的Bear和Raccoon) 在先,实际上应该在后。只有伴随着Panda 的声明,虚拟继承才是必要的。但是,如果Bear 和Raccoon 还没有实现虚拟派生,则Panda 类的设计者就不走运了。
这是否意味着,我们应该尽可能地以虚拟方式派生我们的基类,以便层次结构中后续的派生类可能会需要虚拟继承,是这样吗?不!我们强烈反对,那样做对性能的影响会很严重(而且增加了后续类派生的复杂性)。
那么,我们从不应该使用虚拟继承吗?不是,在实践中几乎所有成功使用虚拟继承的例子中,凡是需要虚拟继承的整个层次结构子树,如iostream 库或Panda 子树,都是由同一个人或项目设计组一次设计完成的。
一般地,除非虚拟继承为一个眼前的设计问题提供了解决方案,否则建议不要使用它。当然,尽管如此,现在我们仍然要看看怎样使用它J
虚拟基类声明
通过用关键字virtual 修正一个基类的声明可以将它指定为被虚拟派生。例如,下列声明使得ZooAnimal 成为Bear 和Raccoon 的虚拟基类:
// 关键字 public 和 virtual 的顺序不重要
class Bear : public virtual ZooAnimal { ... };
class Raccoon : virtual public ZooAnimal { ... };
虚拟派生不是基类本身的一个显式特性,而是它与派生类的关系。如前面所说明的,虚拟继承提供了“按引用组合”。也就是说,对于子对象及其非静态成员的访问是间接进行的。这使得在多继承情况下,把多个虚拟基类子对象组合成派生类中的一个共享实例,从而提供了必要的灵活性。同时,即使一个基类是虚拟的,我们仍然可以通过该基类类型的指针或引用,来操纵派生类的对象。例如,尽管Panda 被设计为虚拟继承层次结构,下面的Panda 基类转换也可以正确执行:
extern void dance( const Bear* );
extern void rummage( const Raccoon* );
extern ostream& operator<<( ostream&, const ZooAnimal& );
int main()
{
Panda yin_yang;
dance( &yin_yang ); // ok
rummage( &yin_yang ); // ok
cout << yin_yang; // ok
// ...
}
如果一个类可以被指定为基类,那么我们就可以将它指定为虚拟基类,而且它可以包含非虚拟基类支持的所有元素。例如,下面是ZooAnimal 类声明:
#include <iostream>
#include <string>
class ZooAnimal;
extern ostream& operator<<( ostream&, const ZooAnimal& );
class ZooAnimal {
public:
ZooAnimal( string name, bool onExhibit, string fam_name )
: _name( name ), _onExhibit( onExhibit), _fam_name( fam_name )
{}
virtual ~ZooAnimal();
virtual ostream& print( ostream& ) const;
string name() const { return _name; };
string family_name() const { return _fam_name; }
// ...
protected:
bool _onExhibit;
string _name;
string _fam_name;
// ...
};
直接派生类实例的声明和实现与非虚拟派生的情形相同,只是要用到关键字virtual。例如,下面是Bear 类声明:
class Bear : public virtual ZooAnimal {
public:
enum DanceType {two_left_feet, macarena, fandango, waltz };
Bear( string name, bool onExhibit=true )
: ZooAnimal( name, onExhibit, "Bear" ),_dance( two_left_feet )
{}
virtual ostream& print( ostream& ) const;
void dance( DanceType );
// ...
protected:
DanceType _dance;
// ...
};
类似地,下面是Raccoon 类的声明:
class Raccoon : public virtual ZooAnimal {
public:
Raccoon( string name, bool onExhibit=true )
: ZooAnimal( name, onExhibit, "Raccoon" ),_pettable( false )
{}
virtual ostream& print( ostream&) const;
bool pettable() const { return _pettable; }
void pettable( bool petval ) { _pettable = petval; }
// ...
protected:
bool _pettable;
// ...
};
特殊的初始化语义
如果在一个派生类中有一个或多个虚拟基类间接出现,那么它就需要有特殊的初始化语义。稍后我们将看一看前面的Bear 和Raccoon 类的实现。你能看出由Panda 类派生引起的问题吗?
class Panda : public Bear,public Raccoon, public Endangered {
public:
Panda( string name, bool onExhibit=true );
virtual ostream& print( ostream& ) const;
bool sleeping() const { return _sleeping; }
void sleeping( bool newval ) { _sleeping = newval; }
// ...
protected:
bool _sleeping;
// ...
};
对,问题在于Bear 和Raccoon 的基类构造函数都提供了一个带有显式实参集合的ZooAnimal 构造函数。更加糟糕的是,在我们的例子中,这个被用作科目名(name)的实参不但不相同,而且对Panda 类无效。
在非虚拟派生中,派生类只能显式初始化其直接基类。例如,在ZooAnimal 的非虚拟派生中,Panda 类不能在Panda 成员初始化表中直接调用ZooAnimal 的构造函数。然而,在虚拟派生中,只有Panda 可以直接调用其ZooAnimal 虚拟基类的构造函数。
虚拟基类的初始化变成了最终派生类的责任,这个最终派生类是由每个特定类对象的声明来决定的。例如,我们在声明Bear 类对象时:
Bear winnie( "pooh" );
Bear 是winnie 对象的最终派生类,它所调用的ZooAnimal 构造函数被执行。当我们写如下语句时:
cout << winnie.family_name();
输出的是:
The family name for pooh is Bear.
类似地,如下声明:
Raccoon meeko( "meeko" );
声明meeko是Raccoon类对象的最终派生类时,初始化ZooAnimal 成为Raccoon类的责任。当我们写如下语句时:
cout << meeko.family_name();
输出的是:
The family name for meeko is Raccoon.
现在,当我们声明Panda 类对象时,比如:
Panda yolo( "yolo" );
Panda 是yolo 类对象的最终派生类,所以初始化ZooAnimal 成为Panda类的责任。
当一个Panda 对象被初始化时,在Raccoon 和Bear 的构造函数执行过程中,它们对于ZooAnimal构造函数的调用不再被执行;ZooAnimal构造函数被调用时,其实参是在Panda的初始化表中被指定的。下面是具体实现:
Panda::Panda( string name, bool onExhibit=true )
: ZooAnimal( name, onExhibit, "Panda" ),
Bear( name, onExhibit ),
Raccoon( name, onExhibit ),
Endangered(Endangered::environment,Endangered::critical)_sleeping(false)
{}
如果Panda 的构造函数没有显式地为ZooAnimal 构造函数指定实参,则发生下面两个动作之一:调用ZooAnimal 的缺省构造函数,或者,如果没有缺省构造函数,则编译器在编译Panda构造函数的定义时会给出一个错误消息。
当我们写如下语句时:
cout << yolo.family_name();
输出的是:
The family name for yolo is Panda.
在Panda, 中Bear 和Raccoon 类都被用作中间派生类而不是最终派生类。作为中间派生类,所有对虚拟基类构造函数的调用都被自动抑制了。如果Panda 又被其他类派生,则Panda也将成为中间派生类,它对ZooAnimal 构造函数的调用也将被自动抑制住。
或许你已经注意到,当Bear 和Raccoon 类被用作中间派生类时,向Bear 和Raccoon 构造函数传递的两个实参是不必要的。避免这种不必要的参数传递的解决方案是,提供一个显式的构造函数,用于“当它被作为中间派生类时”的情形。例如,中间类Bear 的构造函数可以修改如下:
class Bear : public virtual ZooAnimal {
public:
// 当作为最终派生类时
Bear( string name, bool onExhibit=true )
: ZooAnimal( name, onExhibit, "Bear" ),_dance( two_left_feet )
{}
// ... rest the same
protected:
// 当作为一个中间派生类时
Bear() : _dance( two_left_feet ) {}
// ... rest the same
};
我们将这个实例指定为protected, 因为它只希望在后续的派生类中被调用。假设我们已经为Raccoon 提供了类似的缺省构造函数,则可以如下修改Panda 构造函数:
Panda::Panda( string name, bool onExhibit = true )
: ZooAnimal( name, onExhibit, "Panda" ),
Endangered( Endangered::environment,Endangered::critical )
_sleeping( false )
{}
构造函数与析构函数顺序
无论虚拟基类出现在继承层次中的哪个位置上,它们都是在非虚拟基类之前被构造。例如,在下面这个有点古怪的TeddyBear 派生类中,有两个虚拟基类:直接的ToyAnimal 实例,以及来自Bear 的ZooAnimal 实例:
class Character { ... };
class BookCharacter : public Character { ... };
class ToyAnimal { ... };
class TeddyBear : public BookCharacter,public Bear, public virtual ToyAnimal
{ ... };
层次结构如下图所示,这里的虚拟派生用虚箭头表示,而非虚拟派生用实箭头表示:
编译器按照直接基类在声明中的顺序,来检查虚拟基类的出现情况。在我们的例子中,BookCharacter 的继承子树首先被检查,然后是Bear,最后是ToyAnimal。每个子树按深度优先的顺序被检查。即,查找从树根类开始,然后向下移动。对于BookCharacter子树,先检查Character,然后是BookCharacter。对于Bear 子树而言,则先检查ZooAnimal,然后是Bear。
在这个查找算法下,TeddyBear 的虚拟基类构造函数的调用顺序是,先ZooAnimal,后跟ToyAnimal。
一旦调用了虚拟基类的构造函数,则非虚拟基类构造函数就按照声明的顺序被调用:先是BookCharater,然后是Bear。在BookCharacter 构造函数执行之前,它的基类Character构造函数先被调用。已知声明:
TeddyBear Paddington;
基类构造函数的调用顺序如下:
ZooAnimal(); // Bear 的虚拟基类
ToyAnimal(); // 直接虚拟基类
Character(); // BookCharacter 的非虚拟基类
BookCharacter(); // 直接非虚拟基类
Bear(); // 直接非虚拟基类
TeddyBear(); // 最终派生类
这里初始化ZooAnimal 和ToyAnimal 是TeddyBear 的责任,因为它是Paddington 类对象的最终派生类。
虚拟基类成员的可视性
让我们重新定义Bear 类,以提供它自己的onExhibit()成员函数的实例(原来的onExhibit()成员实例从ZooAnimal 继承而来):
bool Bear::onExhibit() { ... }
通过Bear 类对象引用的onExhibit()现在被解析为Bear 的实例:
Bear winnie( "a lover of honey" );
winnie.onExhibit(); // Bear::onExhibit()
通过Raccoon 类对象引用的Raccoon meeko( "a lover of all foods" );
Raccoon meeko( "a lover of all foods" );
meeko.onExhibit(); // ZooAnimal::onExhibit()
派生类Panda 从它的两个基类所继承而来的成员可被分为以下三类:
1 ZooAnimal 虚拟基类实例,如name()和family_name(),它们没有被Bear 和Raccoon改写。
2 继承自Raccoon、属于ZooAnimal 虚拟基类的onExhibit()实例,以及Bear 定义的、被改写了的onExhibit()实例。
3 继承自ZooAnimal、分别被Bear 和Raccoon 特化了的print()实例。
对于这些继承得到的成员,哪些可以在Panda 类域中被直接地、无二义地访问?在非虚拟派生下,答案是没有,所有非限定修饰的引用都是二义的。在虚拟派生下,第1 项和第2项的所有成员都可以被直接地、无二义地访问。例如,已知Panda 类对象:
Panda spot( "Spottie" );
下面的调用
spot.name();
调用了共享的ZooAnimal 虚拟基类成员函数name()。而下面的调用:
spot.onExhibit();
调用了派生的Bear 成员函数onExhibit()。
当两个以上的成员实例分别通过不同的派生路径被继承(不但适用于成员函数,也适用于数据成员和联套类型),并且它们都代表了相同的虚拟基类成员时,则不存在二义性,因为它们共亭了该成员的单个实例(第1 项)。如果一个代表虚拟基类的成员,而另一个是后续派生类的成员,则也不会有二义性(特化的派生类实例的优先级高于共享的虚拟基类实例[第2 项])。但是,如果它们都代表后续派生类的实例,则直接访问该成员就是二义的。最好的解决办法是在派生类中给出一个改写的实例(第3 项)。
例如,在非虚拟派生下,通过Panda 类对象对onExhibit()的非限定修饰引用就是二义的:
// 错误: 在非虚拟派生下二义
Panda yolo( "a lover of bamboo" );
yolo.onExhibit();
在非虚拟派生下的解析引用过程中,每个继承得到的实例都具有同样的权值,所以未限定修饰的引用将导致编译时刻二义性错误。
在虚拟派生下,对于虚拟基类成员的继承比“该成员后来重新定义的实例”的权值小。继承得到的Bear 的onExhibit()实例,比通过Raccoon 继承得到的ZooAnimal 实例优先:
// ok: 在虚拟继承下没有二义
// 调用 Bear::onExhibit()
yolo.onExhibit();
如果在同一派生级别上有两个或多个基类重新定义了一个虚拟基类成员,则在派生类中,它们有相同的优先级。例如,如果Raccoon 也定义了一个onExhibit()成员,则Panda 需要用适当的类域操作符来限定修饰每个访问:
bool Panda::onExhibit(){
return Bear::onExhibit() && Raccoon::onExhibit() && ! _sleeping;
}