21 Multiple and virtual inheritance
Bhushan Trivedi
Introduction
In the previous two modules, we have seen how different types of inheritance are specified and used. We have also seen how base class members are treated in the derived class. We have seen the cases of three different types of inheritance and also seen how the base class subobjects are constructed and inserted in the derived class to provide uniform performance for accessing all base and derived class members in the derived object. We have also seen how the embedding the base class subobject concept can be replaced by more flexible but slower and biased towards derived class member approach. We have also seen the consequences of derived a class further and deriving a class into multiple classes. In this final module on the inheritance process, we will look at two of the most complex features, the multiple-inheritance, a way of inheriting a class from more than one parent and virtual inheritance, a method of inheriting while avoiding a duplicate copy of a typical base class, defined as virtual.
Multiple Inheritance
In all examples above, we only have one class derived from another. It is possible that a class is derived from multiple classes. Consider a class called IndianCricketer which defines cricket players who are Indians. If there are two classes Indian and Cricketer which are already defined, it is natural for the IndianCricketer class to inherit from both of them at the same point in time as it inherits the properties of both, Indian as well as a cricketer, at the same point in time. There are many such cases exists in real world, for example, a sports student class inherits from sports person and student classes, Jeans pants inherits from both jeans at the same time as the pants, a male student inherits from a male as well as a student class etc. One can easily understand if the relationship is of multiple-inheritance by checking if the isa connection is true from the child to both parents at the same point in time. In short, when a single derived class is inheriting from more than one base class, that type of inheritance is known as the multiple-inheritance. Multiple-inheritance, thus, is about inheriting from more than one base class into a single class. In other words, the multiple-inheritance is about entities which inherit attributes from more than one distinct entities. Sometimes there is a confusion between specialization and multiple-inheritance. For example, a luxury car. Should it inherit from luxury products and cars? Normally, it is not right. It is a specialization of the car as it adds a few additional items which are not found in normal cars. However, it does not inherit most things from a luxury product class. However, one can easily look at the thumb rule specified above related to isa link to check if there is a genuine multiple-inheritance relationship.
Let us take a case of SportsStud class which is multiply inherited from SportsPerson class as well as Stud class. Stud class is the same as we have seen before. SportsPerson class contains sport he is playing, details about national and international sports events, medals won etc. There are few other academic fields like sports subjects taken, sports points obtained and event participation which is typical of a sports student class. So there are three different set of attributes, student attributes derived from Stud, Sports related attributes related to SportsPerson class, and attributes which are related to the multiple derived class SportsStud class.
Let us define the word inheritance chain before we proceed further. Inheritance chain is a sequence of classes indicating how the current class is inherited from its parent class and how the parent class is inherited from its own parent and so on.
Here is the example to illustrate how multiple-inheritance multiple in this case.
//Program 22.1
//MultipleInheritance.cpp
#include <iostream>
#include <string>
using namespace std;
const int Sports = 3;
class Stud
{
private:
string Nm;
string Add;
char Grade;
string NmSchool;
public:
Stud(string t_Nm, string t_Add,char t_Grade, string t_NmSchool)
{
Nm = t_Nm;
Add = t_Add;
Grade = t_Grade;
NmSchool = t_NmSchool;
}
};
class SportsPerson
{
private:
string Sport;
string Events;
string Exp;
int Age;
public:
SportsPerson(string t_Sport,string t_Events,string t_Exp,int t_Age)
{
Sport | = | t_Sport; |
Events | = | t_Events; |
Exp | = t_Exp; | |
Age | = t_Age; |
}
};
class SportsStud:public SportsPerson, public Stud
{
private:
string SportsSubs[Sports];
int SportsPts;
public:
SportsStud(string t_Nm, string t_Add,char t_Grade, string t_NmSchool,string t_Sport,string t_Events,string t_Exp,int t_Age,int t_SportsSubs[],int t_SportsPts)
:Stud(t_Nm, t_Add, t_Grade,t_NmSchool),
SportsPerson(t_Sport,t_Events, t_Exp, t_Age)
{
for(int i=0;i<Sports;++i)
SportsSubs[i]=t_SportsSubs[i];
SportsPts = t_SportsPts;
}
};
int main()
{
int SportsMarks[] = {59,78,67};
SportsStud Jay(“Jay Trivedi”,”India”,’A’,”Sardar”,”PowerLifting”,”Gold”,”3″,25,SportsMarks,50);
}
In above program 22.1, we have two classes, a Stud class as well as a SprotsPerson class. We have a SportsStud class inherited multiply from both of these classes. Kindly observe the syntax when a class is deriving from multiple classes.
class SportsStud:public SportsPerson, public Stud
The syntax is straightforward, all classes which the class being defined is inheriting from, are written in comma separated manner after the class header and a colon. Once we do that, the class being defined inherits multiply from all these classes. In this example, we have two classes but it can be more. There is no upper limit specified by the standard about how many classes a class can inherit itself but normally a compiler sets some limits which is much bigger than what we need in a real case. You can see that both classes are publicly inherited but other forms are also possible to be used. Public derivation, however, is the most common case.
Multiple-inheritance, thus, is about inheriting from more than one base class into a single class. In other words, the multiple-inheritance is about entities which inherit attributes from more than one distinct entities. When we find such real-world cases of natural multiple-inheritance, mapping them in the language becomes easy and intuitive because of the facility provided by the language. Some languages, most notably Java, does not provide multiple-inheritance. There are reasons for the omission. (Java does not have pointers either). Let us try to see what is the problem with multiple-inheritance which prompts languages like Java to omit it.
Issues with Multiple Inheritance
Some designers and programmers are of the opinion that multiple inheritance or MI for short, is a bad feature and they normally discourage the practice of using MI. C++, however, supports it. One must understand why C++ does so. C++ designers assume that the programmer is capable to choose what is right and what is wrong in a given situation andprovide them all possible flexibility, for example, it provided pointers. MI is quite similar to pointers. One can make a mess of a program if implement MI incorrectly. As a programmer, C++ does not discourage you to use MI but you must do it after understanding the consequences of using it. Let us try to understand what exactly the opponents say about MI.
There are two views on the MI which advocates not using it. One is from the design point of view and another is from the implementation or from the compiler’s point of view. One must look at the problem while designing solution and determine whether one needs a design with MI. It must address typical MI issues while designing. Another point is MI is not easy for compilers to implement. A lot of processing is to be done by the compiler to implement MI and there is a chance that it might reduce efficiency a bit. C++ compiler writer has to do a much more complex job because the language provides MI support as compared to other language compiler writers where the language does not provide such a support. Let us try to elaborate both design level and complier-level issues with the MI.
The idea of describing these issues is definitely not to discourage readers from using MI but to have them a clear understanding of MI and be ready to answer queries from others when they advocate using MI in their programs.
compiler needs to work harder
We have seen that the base class members are stacked at the beginning of the derived class as a base class subobject and are accessible to the derived class and members based on their type of inheritance. When a class is inherited once and further inherited, it follows natural chain by adding the content on the top every time the class is inherited. Unfortunately, that natural chain is broken when the class is multiply inherited. Every class which is multiply inherited has more than one parent and makes the job of the compilers difficult when base class subobject is to be extracted from the derived class object. In natural case, it is the topmost part so splicing the top part of the derived object according to the size of base class subobject serves the purpose. Unlike that, in case of multiple-inheritance, apart from the first class in the group of parents, all other parents subobjects cannot be just slicing based on the size but need to be extracted carefully based on the position of the specific subobject in the derived class structure. The position of any other subobject than the first one is determined by determining upper and lower boundaries of the subobject in the derived class object. The upper boundary is determined by the size of all data members of the parents appear in the inheritance chain before the specified class and lower boundary is determined by the size of the very subobject. This process, based on how the program is written might happen at the runtime (when the type of the object the pointer pointing to is only available at runtime) and thus even add runtime overhead to the program. In fact, the order in which the classes are mentioned in the inheritance chain is also important. Their subobjects laid out in the derived object in exactly the same order. The extraction of the subobject of any of them is based on the position and the position is based on the order in which they are listed. Let us try to see it as an example.
Consider two cases of a base and derived class definitions as depicted in the figure 22.1.
Class Base {..};
Class Derived: public Base {..};
Class DD: public Derived{..};
Class Base1 {..};
Class Base2 {..};
Class MD: public Base1, public Base2 {..};
One must understand that the type of pointers makes a huge difference in accessing the dereferenced object. It is possible to access a derived class object by a base class pointer but it can only access the base class subobject part. That means, if a base class pointer and a derived class pointer are pointing to the same derived class object, their span of access is different. Once the reader is clear about that point, the following section explains the need for compilers to intervene in the case of multiple-inheritance.
Figure 22.1 depicts how those classes subjects are laid out. In the top part, the normal further derivation is described and in the bottom part, the multiple-inheritance is shown. It is quite possible that the programmer writes following statements in the program.
DD oDD;
pBase *B = &oDD;
pDerived *D = &oDD;
In both of above assignments, the compiler does not need to intervene. The address of both the base class and derived class subobject is the SAME as the DD object. The length of the object, however, is different but that is managed by pointing to by the right type of pointer. For example, a base class pointer will only span the size of the base class while the derived class pointer will span the complete derived class subobject. When the assignment takes place to the typical type of pointer, the lower part of the object is automatically neglected and the right type and size of the object is addressed by the pointer.
Now, look at the following set of statements.
MD oMD;
pBase1 *B1 = &oMD;
pBase2 *B2 = &oMD;
Can compiler assume the same and allow the address assignment? No. In the case of the pBase1 pointer, fortunately, it is about the first class in the inheritance chain and compiler does not need to intervene but in the case of pBase2, the compiler cannot keep the same address. It has to add offset to the address to move it to the beginning of the base class 2 subobject in the structure of the MD object. You can clearly see that if we have more such classes inherited multiply, the subobject of the specific class is placed at a different location than the object itself and the compiler must provide proper offset while converting such statements to the object code. Isolation of a typical part from the derived object requires compilers to calculate and position the pointer to the exact address from where the required subobject begins. As mentioned before, if the type of the pointer is only learned at the runtime, all these pain is to be taken at the runtime which adds substantial overhead.
The attribute to inherit issue
The normal inheritance is useful for one typical case. When the required information is not available with the derived class, sometimes it is available with the base class and traversing up in the inheritance chain may get an answer. Consider a popular case of a pet bird Fifi, which is also an ostrich. Fifi is an object of the class which is multiply inherited from two different classes, a pet-bird class, and another class ostrich. That means Fifi is a pet-bird as well as an ostrich. While a query about the fly attribute is asked and the Fifi object does not contain any information, one would like to check with the parent class with that value of the attribute. Here we have two options to traverse up, through the pet-bird class or through the Ostrich class. If we travel through pet-bird, which is derived from the bird, yields fly = yes while if we travel up through Ostrich, we receive the answer fly = no as ostriches do not fly, unlike normal birds. One needs a better algorithm to choose between these two answers. In fact, there are some working algorithms which find the answer coming from a more specific class and choose that one as right. In above case, the ostrich is more specific than the bird class and that answer fly = no will be accepted as valid. In short, the multiple-inheritance allows the class to inherit attributes from more than one classes when both parent classes have a similar or same attribute, which attribute or value of which attribute to inherit becomes a complex issue sometimes. Such a hierarchy is known as a tangled hierarchy and the issue is known as the attributes to inherit issue.
Above example is quite simple, there may be more than parent classes, multiple paths to a typical parent exists and contradicting values of a given attribute is possible to be obtained. This problem, in many cases, is much harder to solve than above case. The final object becomes cluttered with multiple attributes inherited from multiple classes and errors are hard to debug. One must avoid multiple-inheritance if possible but should use when it can be used to improve the readability of the solution.
Multiple copies of multiple inherited base class
Further inheritance, as we have seen so far, is quite easy. However, sometimes users find a typical problem when further inheritance and multiple-inheritance used together. This is a case when there is a base class from where two derived classes are inherited and those two derived classes multiply inherited into another class. Here is an example.
Class Base {..};
Class Derived1: public Base {..}
Class Derived2: public Base {..}
Class MD: public Derived1, public Derived2 {..}
If you carefully examine above definitions, you will learn that both Derived1 and Derived2 are inherited into MD and inadvertently carrying with them a copy of the Base object in MD. The structure of MD looks as depicted in the figure 22.2. Though MD has two copies of the base class subobject, they are different instances and are actually different entities of the same type. That means they can have different values associated with them at the same point in time.
The MD class has a copy of the Derived1 class object and Derived2 class object, as it is multiply inherited from both of them. However, both of these classes contain their own copy of the Base class subobject which is copied twice in the MD class. You can see that Derived1 is actually a combination of Base class subobject plus its own additional members while Derived2 is a combination of a Base class subobject plus its own additional members. When they are combined together in MD, the Base class subobject of Derived 1 and Base class subobject of Derived 2 are copied in MD1.
- 1 Both copies may not be exactly same as they are two different instances of the same type of object
That means, If, in a multiple derived the class, two of the classes inherit from the same class and thus have same base class object embedded within, will make the multiple derived class to have two base class subobjects inherit and thus having double (but not necessarily duplicate) copies of the base class subobject. This issue is called double copies issues. Whenever there are multiple base classes, this issue is multiple base class copies issue. We will not stress this point further but the subsequent discussion will only talk about two base classes which can also be applied to a case of multiple base classes.
Double or multiple copies of the base class subobject may or may not be a problem. For example, if we have a vertical scrollbar and horizontal scroll bar inherited from scroll bar, and we inherit both vertical and horizontal scrollbar in screen scroll bar (where both scrollbars are to be shown separately), we do not mind double copies of both objects. We want, for example, two copies of the attribute called position to have two different values with respect to the horizontal and vertical scroll bar. However, it is not the case always and we would like to eliminate double copies of the base class. Virtual base classes are the ones which avoid such double copies of themselves.
Figure 22.2 describes the case where we have multiple-inheritance with two copies of the base class subobject, one from Derived1 and another from Derived2 class. You can also see that both copies are at different places, at the beginning of their respective parent class objects. The final MD object contains these multiple copies of the base class subobjects eventually.
Program 22.2 showcases the implementation of the multiple-inheritance using classes as specified in the figure 22.2. You can see that the definition of the by is as per the diagram. However, one part that catches your eye may be the qualification of the base class subobjects as follows.
DD.Derived1::bInt = 0;
DD.Derived2::bInt = 10;
Both bInt members are basically the content of the base class being inherited twice. We are able to address both of them using this qualification. DD.Derived1 indicates the subobject of Derived1 class while DD.Derived2 indicates the subobject of the Derived2 class. If we do not qualify the bInt value, the compiler is confused as to which one of the two values it is referring to and won’t compile any statement which refers to bInt in that fashion. That is the reason following statement was to be commented.
//DD.bInt = 0;
The code above refers to bInt which can be one of the two possible and compiler has no way of knowing which and that is why it objects to this reference.
Here is the code.
//program 22.2
//NonVirInherit.cpp
#include <iostream>
#include <string>
using namespace std;
class Base
{
public:
int bInt;
};
class Derived1 : public Base
{
public:
int D1Int;
};
class Derived2 : public Base
{
public:
int D2Int;
};
class MD : public Derived1, public Derived2
{
public:
int MDInt;
};
int main()
{
MD DD;
//DD.bInt = 0;//error: ambiguous access of ‘bInt’ in ‘MD’
DD.Derived1::bInt = 0; // now it is not ambiguous
DD.Derived2::bInt = 10; // Ya! it is different than previous bInt
cout << DD.Derived1::bInt<<“\n”;
cout << DD.Derived2::bInt<<“\n”;
}
The output is
0 10
As mentioned before, one can have a similar set of classes for vertical and horizontal scroll bars and it is perfectly fine there. Here is a snippet.
Class Scroll {int Position;…};
Class vScroll: public Scroll{..};
Class hScroll: public Scroll{..};
Class ScreenScroll: public vScroll, public hScroll; {..}
ScreenScroll.vScroll:Position = t_V_Position;
ScreenScroll.hScroll:Position = t_H_Positiion;
You can see that the code is perfectly fine and have two different values of position attribute is perfectly fine with the user’s specification.
Virtual base classes
Not every case demands two different copies of the base class. What should we do to avoid generation of double copies of the base class? Just by doing one thing, precede the declaration of the derived class in the header with word virtual.
The order of the words public and virtual is not important in the definition and thus public virtual and virtual public is the same. Once we make the inheritance virtual, the compiler does not add the second copy of the base class and virtually provide that effect. That means, despite having only one copy of the base class, the programmer can access that base copy as a subobject belongs to the Derived1 class or a subobject belongs to the Derived2 class or just mention it without any qualification, the same base class subobject is referred. In other words, a class which is inherited with a special word virtual either preceded by or appended by to the access specifier during inheritance. The compiler keeps a tab on this class and whenever more than one copy of the said base class is found in the multiple derived class, the compiler makes sure that the other copy is only virtually present and no physical copies to save memory. Such a class (whose double copy problem is avoided by the compiler) is called the virtual base class.
Here is our modified program.
//VirInherit.cpp
#include <iostream>
#include <string>
using namespace std;
class Base
{
public:
int bInt;
};
class Derived1 : public virtual Base
{
public:
int D1Int;
};
class Derived2 : virtual public Base
{
public:
int D2Int;
};
class MD : public Derived1, public Derived2
{
public:
int MDInt;
};
int main()
{
MD DD;
DD.bInt = 0;//No error now it is same as following two
DD.Derived1::bInt = 0; // same as other two
DD.Derived2::bInt = 10; // it is changing its value
cout << DD.Derived1::bInt<<“\n”;
cout << DD.Derived2::bInt<<“\n”;
cout << DD.bInt;
}
The output is
10 10 10
It will print all three values the same as 10 as all three assignment statements refer to the same member. The DD.bInt, DD.Derived1::bInt , DD.Derived2:: bInt are all referring to the same member, now exists only as a single instance.
Can you guess what exactly the compiler needs to do to ensure a single copy in such a case? A compiler always tags the classes inherited as virtual especially. Whenever they are further inherited, the compiler checks the path of inheritance. If the inheritance path includes more than one copy of the same base class (belongs to virtually inherited classes), it will only provide one copy of the base class by slicing all but first copy of the derived class and removing the base class subobjects from those classes. Here is the diagrammatical representation of the idea depicted in figure 22.3. The compiler has to slice the base class subobject from the Derived2 object and add only that sliced part is copied in the MD class.
You can see that the compiler, as depicted in figure 22.3 have removed the base class subobject from Derived2 class object before constructing the MD class. This is because the compiler has kept the track of the Derived2 class and it has learned that the Base class is inherited in a virtual fashion. When the compiler learns about the duplicate copy of the same in the MD class, it eliminates that by this slicing operation. Thus compiler ensures that two instances of the same base class subobject is not found in the resultant MD class object.
Virtual base classes implementation
Implementing virtual base classes is not limited to generating the multiply inherited class (the MD class) in above situation, but also provide proper meaning to assignments that users expected to make. It is quite a challenge for a compiler writer to interpret the code involving a virtually inherited base class and the code which does not in the consistent and unambiguous fashion, especially when one type of object is assigned to another.
Consider following assignments.
Derived2 oD2, *pD;
MD oMD;
Base *pB;
Now consider following assignments;
pB = &oD2;
pB = &(oMD::Derived2); // Get the base class from somewhere else pD = &(oMD::Derived2); //Collect scattered part of the object
in above case, whenever pB is pointing to oD2, the base subobject is easy to extract, as it forms the top part of the oD2 object. However, in the second case, it is not so as the oMD’s Derived2 subobject does not contain the base class subobject at the beginning of that Derived2 subobject. It will have to be extracted from the top of the Derived1 subobject’s top. The compiler needs to intervene in any situation of this type and make proper changes in the object code as to provide proper assignments to objects and pointers. Consider the final assignment, we need a pointer to the Derived2 object. It is a much harder job as the object is scattered in two parts (can be in multiple parts if it is inherited multiple times). The compiler needs to synthesize those parts and represent them as a single object as and when used in the program. As mentioned before, there are cases where the pointer is pointing to is not available at the time of compilation and the decision is to be made at runtime. In that case, it adds to the runtime overhead.
That is the reason virtual base classes are much harder to be implemented as compared to normal base classes. We will throw some light on the issue while discussing the C++ object model in much more detail in modules 34,35 and 36. For more detailed information, consider referring to the “Inside the C++ Object Model” by Stanley Lippmann.
The order of constructors and destructors calls
When there are multiple classes from where the then being defined is inherited, the constructors invoked have to follow some simple rules. Here they are.
- If the base class constructor is defined, the derived class must provide its own constructor. The reason is, if the derived class constructor is not provided, how will it call the base class constructor? If the programmer fails to provide, the compiler generally provides one by itself but it will only satisfy the compiler’s requirement and not the programmers. We will delve deeper into this issue in last three modules where we study the C++ object model in little more detail.
-
- When the object oDn or Bn are destroyed, the destructors are called exactly in reverse order of their definition.
- If we have class definitions as follows, we consider an inheritance chain is produced from class B to class Dn. class D1: public B; class D2: public D1; class D3: public D2… class Dn: public Dn-1. In this case, when we define an object of class Dn, for example,
- The exception to the above rule is, if any of the classes from B1 to Bn is inherited virtually, it is called before others
- Suppose if we have class Derived: public B1, public B2 … public Bn {..}. In that case, the constructor for class B1 is called first, class B2 is called second and so on till the class Bn, using MIL. Even if the constructors are defined in the header in any other order, the execution will always follow the order of inheritance. Dn oDn ;
- The exception to the above rule is, if any of the classes from B1 to Bn is inherited virtually, it is called before others
The constructor of class B is called first, the constructor of class D1 is called next, D2 follows it and so on till the constructor of Dn.
- If we have class definitions as follows, we consider an inheritance chain is produced from class B to class Dn. class D1: public B; class D2: public D1; class D3: public D2… class Dn: public Dn-1. In this case, when we define an object of class Dn, for example,
In short, the inherited class will call the constructors of the base class in the order specified in the inheritance chain except for the case where the base class is virtual. The virtual base class constructors are invoked before non-virtual base class constructors.
Summary
In this module, we have looked at two of the most confusing and complicated types of inheritance, the multiple-inheritance as well as the virtual inheritance. Multiple-inheritance allows a class to inherit from more than one class. Virtual inheritance allows a class to have only one subobject even when copied multiple times in the multiple derived class. We have seen that both these types of inheritance complicate the manner in which the compiler normally works and the compiler needs to intervene when users produce statements logical from their understanding but not so internally.
- When the object oDn or Bn are destroyed, the destructors are called exactly in reverse order of their definition.