C++多重继承、钻石继承问题及其可行的优化方法
最近正在阅读C++经典之作,由Scott Meyers所著的《Effective C++》,不得不说,大师永远是大师,对C++的见解十分独特与深刻,其中所涉及的条款与经验之谈给于了我许多启发和想法。目前阅读到本书的第四十条款,《Use Multiple Inheritance Judiciously》,即“ 明智而谨慎的使用多重继承”,以前基本没有接触过多重继承,因此特地做此笔记。
第一次发稿,如有错误或不周,欢迎各位指正。
1.什么是多重继承和钻石型多重继承
多重继承(Multiple inheritance; MI)的定义:多重继承是包括 C++ 在内的一些面向对象编程语言的一项功能,它允许一个类从多个基类继承。这意味着一个派生类可以从多个父类继承属性和方法,从而使其能够组合和重用来自多个来源的功能。
1.1 多重继承
举个案例:
MP4plyaer.h
#ifndef MP4PLAYER_H
#define MP4PLAYER_H
#include<iostream>
#include<string>
//定义了一个管理音乐的类,成员有音乐的名字,大小和格式,成员函数则为返回其上述属性以及音乐的播放功能的实现。
class Music{
public:
Music(const std::string& Name,float Size,const std::string& Form ):Music_Name(Name),Music_Size(Size),Music_Form(Form){}
Music(const Music& item):Music_Name(item.Music_Name),Music_Size(item.Music_Size),Music_Form(item.Music_Form){}
float Msize(){return Music_Size;}
std::string Mform(){return Music_Form;}
std::string MName(){return Music_Name;}
void Play_music(){std::cout<<"Play music right away!!";}
private:
std::string Music_Name;
float Music_Size;
std::string Music_Form;
};
//定义了一个管理视频的类,成员有视频的名字,大小和格式,成员函数则为返回其上述属性以及视频的播放功能的实现。
class Video{
public:
Video(const std::string& Name,float Size,const std::string& Form ):Video_Name(Name),Video_Size(Size),Video_Form(Form){}
Video(const Video& item):Video_Name(item.Video_Name),Video_Size(item.Video_Size),Video_Form(item.Video_Form){}
float Vsize(){return Video_Size;}
std::string Vform(){return Video_Form;}
std::string VName(){return Video_Name;}
void Play_Video(){std::cout<<"play video in seconds!!";}
private:
std::string Video_Name;
float Video_Size;
std::string Video_Form;
};
//定义了一个Mp4播放器,一个正常的MP4播放器可以播放多种格式的音乐与视频,因此定义了了一个Mp4播放器的类,并继承了上述的类(当然,此处采用private继承更加合理)。
class MP4player:public Music,public Video{
public:
MP4player(const Music& item1,const Video& item2):Music(item1),Video(item2){}
void Play(){Play_music();Play_Video();}
/*其他功能*/
};
#endif
main.cpp
#include "Mp4Player.h"
int main()
{
Music Music_Obj("A",1024,".WAV");
Video Video_Obj("B",2048,".Mp4");
MP4player Mp4player_obj(Music_Obj,Video_Obj);
Mp4player_obj.Play();
return 0;
}
/*输出结果:
Play music right away!!play video in seconds!!
*/
利用UML图像来表示的话,就是如下图所示的一个很简单的三角形框架:
如同书中所说的那样,“多重继承的意思是继承一个以上的base classes,但这些base classes并不常在其继承体系中又有更加高级的base classes,因为这样会导致更加致命的“钻石型多重继承”(Diamond Problem)”
1.2 钻石型多重继承
以上述案例为基础,假设音乐类(class Music)和视频类(class Video)它们都继承于同一个父类文件类(class File),那么此时情况就会变得更加复杂。假设文件类定义如下:
MP4plyaer.h
class file
{
public:
virtual const std::string& Rdisk_Formation()const {return disk_formation;}
/*其他功能*/
private:
std::string disk_formation;
/*其他私有成员*/
};
class Music:public file
{/*定义同上*/}
class Video:public file
{/*定义同上*/}
class Mp4Player:public Video,public Music
{/*定义同上*/}
main.cpp
/*内容同上*/
Mp4player_obj.Rdisk_Formation();
如果按照上述的声明继承架构,则编译器将无法通过编译,原因:class Music和class Video均分别继承了一份base class file类(包括public接口和私有成员),此时直接调用Rdisk_Formation()则会导致编译器无法确定那个类所继承的函数为最优调用函数。
上述的分析指出了问题所在, 即class Mp4Player类所继承的两个基类各有一份class file的实例,进而导致了错误。 因此,我们需要让两个基类共享同一份class file实例,我们需要引入虚继承技术(Virtual Inheritance)。
修改结果如下:
class file
{};
class Music:virtual public file
{/*定义同上*/}
class Video:virtual public file
{/*定义同上*/}
class Mp4Player:public Video,public Music
{/*定义同上*/}
这样,Mp4Player所继承的两个基类class Music和class Video都共享同一份它们的父类class file的实例,这样便顺利解决了问题。
1.3 钻石型多重继承的性能负担
但是,天下没有免费的午餐,引入Virtual Public Inheritance后会对程序的性能造成的主要从负担为:
1.额外的内存消耗
为了维持由虚继承得到的派生类与共享基类之间的联系,需要额外的指针(通常被称为v-pointers或者虚拟表指针(virtual table pointers))。
2. 非直接访问导致的消耗
当需要访问虚基类中的成员或者成员函数时,需要使用V-pointers定位虚基类共享实例的位置,相比较于普通的public继承的直接访问来说,它需要进行非直接访问。
3.初始化复杂度
涉及到虚继承的类需要小心地定义其构造函数,必须得遵守的定则为:虚基类必须通过"最新"的derived class进行初始化。还是以Mp4Player的案例进行说明:
/*为了便于说明情况,为各个类补加了一些复制构造函数*/
class file
{
public:
file(const std::string& Disk_Formation):disk_formation(Disk_Formation){std::cout<<"The base class is initialized!!the value of disk_information is "<<disk_formation<<std::endl;}
/*
其余代码同上
*/
};
class Music:virtual public file{
public:
Music(const std::string& Name,const std::string& file_formation):file(file_formation),Music_Name(Name),Music_Size(0),Music_Form(""){std::cout<<"The Music Class is initialized!!"<<std::endl;}
/*
其余代码同上
*/
};
class Video:virtual public file{
public:
Video(const std::string& Name,const std::string& file_formation):file(file_formation),Video_Name(Name),Video_Size(0),Video_Form(""){std::cout<<"The Video Class is initialized!!"<<std::endl;}
/*
其余代码同上
*/
};
class MP4player:public Music,public Video{
public:
MP4player(const std::string& Music_name,const std::string& Video_name,const std::string& Disk_Form):file(Disk_Form),Music(Music_name,Disk_Form + "Music"),Video(Video_name,Disk_Form + "Video"){}
/*
其余代码同上
*/
};
//利用定义的复制构造函数初始化一个Mp4Player对象试一试
MP4player MP4player_virtualInheritance("ABC","EDF","EXT32");
//并打印储存该Mp4Player对象的硬盘格式信息
std::cout<<"\nThe formation of the current disk is :"<<MP4player_virtualInheritance.Rdisk_Formation();
/*以下为输出结果
The base class is initialized!! the value of disk_information is NTFS
The Music Class is initialized!! The param used for initializing file class is NTFSMusic
The Video Class is initialized!! The param used for initializing file class is NTFSVideo
The formation of the current disk is :NTFS
*/
可以看出,底层类型“file”是由“Most Derived”的派生类Mp4Player的构造函数来显式地调用Base class的构造函数来进行构造的,避免了中间派生类“Music”与“Video”二次构造file类,解决了多重初始化(Multiple Initializations)和潜在的初始化冲突的问题(例如,某个中间派生类在初始化base类的时候,覆盖了其它中间派生类的初始化结果),保证了共享的基类在各个派生类中的一致性和初始化可预测性。
当然,如果忘记了在most derived class中初始化基类,某些编译器可能会弹出编译错误,提醒程序员必须提供相应的初始化操作。
2.改进思路
由于使用virtual base classes所带来的性能损失和可能造成的初始化问题等等因素,最好是能够避免使用virtual base classes和虚继承(Virtual Inheritance)技术。上述的简单案例中,可以将基类file中的成员变量“disk_formation”和其他变量均移动到Video类和Music类中,并舍弃掉基类file避免钻石继承问题的存在。
其次,通过观察继承架构和结合现实日常生活的经验可以发现,Mp4播放器的功能包含了播放音乐和播放视频的功能,即Mp4播放器应当能够实现音乐和视频所具有的属性与功能,但是反过来就不行。也就是说,Mp4播放器“包含了”音乐和视频。
因此,上述代码我们所使用的Public继承并不是非常合理,因为Music类和Video类理应成为Mp4播放器代码实现的一部分组成,为此,采用private继承更为合理。然而,在编程过程中,可能存在这一种更加现实的情况:我们已经拥有了Music和Video类的旧定义,不需要再重新从零开始再写一遍,而是需要对对Music类和Video类的接口进行封装或者修改。以下是一种被书中称为“继承+复合”的方法。
class Music{
public:
Music(const std::string& Name):Music(Name,0," "){};
virtual void Play_music(){std::cout<<"Play music right away!!";}
/*
其余代码同上
*/
};
class Video{
public:
Video(const std::string& Name):Video(Name,0," "){};
virtual void Play_Video(){std::cout<<"play video in seconds!!";}
/*
其余代码同上
*/
};
class Mp4Player
{
public:
virtual void Playmusic() = 0;
virtual void PlayVideo() = 0;
virtual ~Mp4Player(){} ;
};
class MP4player_secondVersion:public Mp4Player
{
public:
MP4player_secondVersion(const std::string &Music_name,const std::string &Video_name):MusicPlayer(Music_name),VideoPlayer(Video_name){}
void Playmusic() override
{
MusicPlayer.Play_music();
}
void PlayVideo() override
{
VideoPlayer.Play_Video();
}
private:
class Mp4player_MusicPlayer:public Music
{
public:
Mp4player_MusicPlayer(const std::string& Music_name):Music(Music_name){}
virtual void Play_music() override
{
std::cout<<"Play the music in ten seconds!"<<std::endl;
}
};
class Mp4player_VideoPlayer:public Video
{
public:
Mp4player_VideoPlayer(const std::string& Music_name):Video(Music_name){}
virtual void Play_Video() override
{
std::cout<<"play video in seconds!!";
}
};
Mp4player_MusicPlayer MusicPlayer;
Mp4player_VideoPlayer VideoPlayer;
};
std::shared_ptr<Mp4Player> CreateMp4Player(const std::string& Msuic_name,const std::string& Video_name)
{
return std::make_shared<MP4player_secondVersion>(Msuic_name,Video_name);
}
MP4player_secondVersion MP4player_object("ABC","EDF");
MP4player_object.Playmusic();
MP4player_object.PlayVideo();
/*输出结果为:
auto Mp4Player_object = CreateMp4Player("ABBB","BCCC");
Mp4Player_object->Playmusic();
Mp4Player_object->PlayVideo();
*/
从上述代码中可以看出,存在一个Mp4Player class作为接口类,Music与Video类作为我们想要构建的Mp4Player的实现细节,其中存在一些virtual函数需要在派生类中重新赋予定义。于是,定义了一个MP4player_secondVersion类并继承接口类Mp4Player,需要在该派生类中实现接口类的执行细节。之后,又设计了两个嵌套类(Mp4player_MusicPlayer,Mp4player_VideoPlayer)并public继承了Music类和Video类,重新定义相应的virtual函数。最后,设计了一个工厂函数,得到一个指向基类的智能指针shared_ptr。
通过这样的设计方法,体现书中的一个思想:“利用public继承需要实现的接口”,“利用private继承某些接口的实现”(当然,此处也可以直接利用private继承Music类和Video类,我所写的案例中所设计的两个嵌套类都是位于private之中,本质上和private继承实现的效果有异曲同工之妙),这样的做法就可以很好的避免了多重继承带来的麻烦,更能规避最不想要的钻石继承的问题。但是嘛,可能的话,还是尽可能的采用单一继承方法,避免设计出多重继承架构,哈哈哈。
最后再补充一点这样设计的优势,如果只希望在MP4player_secondVersion类中重新定义Music类和Video类的virtual函数,而不希望MP4player_secondVersion类的派生类对这些virtual函数进行重新定义,那么上述的设计方法是一种可取的方法。
(当然,现代C++ 11已经引入了final修饰词,可以用来阻止派生类对类或者virtual函数等进行重新定义)