20 Inheritance- the inside view
Bhushan Trivedi
Introduction
What is happening under the hood when the inheritance facilities are provided by any language, especially C++? The programmer must learn about how things are implemented from within as to take full advantage of them. Such an inside view is also important when the programmer encounters an error which seems unreasonable, if he has the insider view, he can understand what probably could lead to that error and has more chances of solving that error. Learning about the C++ object model helps a lot in doing so. We will look at the object model in last three modules of this course (Module 34,35 and 36) but we will also try to throw some light on that in this module. We will also see the consequences of implementing the inheritance in a specific manner. We will be looking at various ways in which programmers can use inheritance and how those things affect the implementation of inheritance. In most cases, C++ chooses speed over other factors, and we will see how that is done.
Introduction
What is happening under the hood when the inheritance facilities are provided by any language, especially C++? The programmer must learn about how things are implemented from within as to take full advantage of them. Such an inside view is also important when the programmer encounters an error which seems unreasonable, if he has the insider view, he can understand what probably could lead to that error and has more chances of solving that error. Learning about the C++ object model helps a lot in doing so. We will look at the object model in last three modules of this course (Module 34,35 and 36) but we will also try to throw some light on that in this module. We will also see the consequences of implementing the inheritance in a specific manner. We will be looking at various ways in which programmers can use inheritance and how those things affect the implementation of inheritance. In most cases, C++ chooses speed over other factors, and we will see how that is done.
C++ Object Model and Inheritance
We have mentioned earlier that the C++ Object Model is the model which specifies how the object code form the C++ source code is generated. It specifies the process of implementing the features provided by the language. In the previous module, we have seen different types of inheritance provided by the C++ language. We have also seen how the access specifiers change for data members based on the way the classes are inherited. In this module, we will begin with how C++ object model implements these facilities. Whatever complex those facilities look like, the C++ object model implements the inheritance process in a simplest possible manner. It embeds the base class subobject within a derived class object to provide all these facilities. When a derived class inherits itself from a base class, whenever a derived class object is defined, a base class object is defined first and inserted as the initial part of the derived class object as a first member, Whatever members the derived class have defined are inserted as members after that base class object (now known as subobject).
Let us define the base class subobject. The base class object which is copied into the derived class when the derived class object is defined is known as the base class subobject. The base class subobject contains the same set of members but with different level of access specification based on how they are originally defined in the base class as well as how they are inherited
Thus following is done when a class inherited.
We have mentioned earlier that the C++ Object Model is the model which specifies how the object code form the C++ source code is generated. It specifies the process of implementing the features provided by the language. In the previous module, we have seen different types of inheritance provided by the C++ language. We have also seen how the access specifiers change for data members based on the way the classes are inherited. In this module, we will begin with how C++ object model implements these facilities. Whatever complex those facilities look like, the C++ object model implements the inheritance process in a simplest possible manner. It embeds the base class subobject within a derived class object to provide all these facilities. When a derived class inherits itself from a base class, whenever a derived class object is defined, a base class object is defined first and inserted as the initial part of the derived class object as a first member, Whatever members the derived class have defined are inserted as members after that base class object (now known as subobject).
Let us define the base class subobject. The base class object which is copied into the derived class when the derived class object is defined is known as the base class subobject. The base class subobject contains the same set of members but with different level of access specification based on how they are originally defined in the base class as well as how they are inherited
Thus following is done when a class inherited.
Derived class object = base class object + members defined in the derived class object
Is the best possible way to implement the inheritance? Well, it depends. If you look at the memory efficiency, it is not a very good idea as it repeats the definition and thus increments the derived class object’s size. Another consequence of this design is that whenever a base class changes its structure, the programs containing the derived class must be recompiled, even when they have no base class object defined within. The reason is the base class subobject needs to be recompiled and that makes the overall derived class object, which is not changed at all, forced to recompile. This is a critical problem when a library contains a base class and the designer of the base class uploads a new version. The user might have a new class derived from one of the classes of that library, which changed its structure, now is forced to recompile his program without changing a single line!
That seems quite ridiculous to a normal user and could be a nightmare for administrators who juggle with multiple versions of source code and when needs to generate the executables once again after recompilation, needs to select the latest version. Those who provide services to other customers, in a case of a base class change, need to update their own product and also need to update the programs at all customer’s premises as well. This design may have serious consequences if the product releases are not properly planned!
A pointer to a base class subobject instead of embedding the base class subobject could have solved this problem. Whenever a base class changes, the pointer will start pointing to a new object and the derived class will not need to be recompiled. This method seems to be more sophisticated unlike the rude way of just embedding the complete base class object inside the derived class object.
Why have C++ designers gone for this ‘rude’ approach? The answer is efficiency. The ‘embedding the base class’ approach is the most efficient approach. C++ designers chose efficiency as their prime criteria for choosing the options available to implement language features. All base class elements, as being embedded in the derived class, are accessed with the same overhead as all derived class members and there is no additional latency. In case of the pointe approach, the derived class members can be accessed directly, but base class members are to be available only after respective pointers are traversed. If a class is inherited further a few times, the inheritance chain gets that long, that many more pointers are to be accessed, and the access to that base class will have that much overhead as compared to derived class members. If we have class 1 inherited from class-2, which in turn inherited from class-3 till the class-N, accessing a class-N element from class-1 needs N pointers to traverse one after another. Accessing an element defined in the class-1 provides direct access without needing to traverse any pointer. Such a performance bias is not what the C++ designers to have in their model. When the base class subobject is embedded, the performance of accessing any member of the derived class, its own or derived, remains uniform.
Can we check if a base class subobject really is inserted int eh derived class object by C++ compiler? Here is a small program which demonstrates so.
//Program 21.1
//EmbeddingBase.cpp
#include <iostream>
using namespace std;
class B
{
int i;
};
class D:public B
{
int j;
};
int main()
{
- B oB; D oD;
cout << ” Size of oB is ” << sizeof (oB) << “\n”;
cout << ” Size of oD is ” << sizeof (oD) << “\n”;
}
output: –
Size of oB is 4
Size of oD is 8
We have two classes, B and D. B has one private member as integer while D inherits from it and also defines one private int member. Ideally, both classes have one int member and thus the size of the objects of each of the classes should be equal to the size of the int. The compiler that the author is using to implement this program provides a 4-byte integer and thus the answer should be 4 in both objects oB as well as oD. The oB shows the answer as expected but the oD does not. The size of oD is shown as 8 bytes. It is because the oB subobject is part of it which adds four bytes to the count. As oB only has one private member i, which, as per logical understanding should not be available to the derived class, but it still has. We have illustrated this point before. The private members are not available directly but indirectly when the public function members of the base class are inherited in the derived class, they are available to the members of the derived class anyway which in turn accesses the private members of the base class.
When the base class subobject is embedded in the derived class, the derived class object size is equal to the size of its own members (j, 4 bytes) + the size of embedded base class subobject (4 bytes) which makes it 8 bytes. This example indicates the code size bloating problem of this model. In short, the C++ object model is quite peculiar about implementing inheritance and providing embedding of all classes of inheritance chain to provide uniform access to all inherited members to that of members which are defined within the derived class. How this simple model affects different types of derivations that we have seen in the previous module? Let us throw some light on the issues.
Public Derivation
Figure 21.1 Base class objet and base class subobject
Base class | Base | Base class | Derived |
object | Private | Subobject | Not accessible directly |
int PrBaseInt | int PrBaseInt | ||
Public | Public | ||
Int PbBaseInt | Int PbBaseInt | ||
Void setPrBaseInt | Void setPrBaseInt | ||
We have seen how public derivation takes place in the previous module. Figure 21.1 depicts how this public derivation is implemented in the C++ object model when compiled. We can see that the private members are copied to the derived class object but not directly visible to either derived class or the member functions of the derived class. The public members of the base class, on the contrary, are available to both of them.
Figure 21.2 Embedding the base class subobject
Derived class structure as per C++ Object model
Not accessible directly
int PrBaseInt | Base | ||
class | |||
Public | |||
subobject | |||
Int PbBaseInt | |||
Derived | Void setPrBaseInt | ||
class | |||
Private: | |||
object | int PrDInt; | ||
public: | |||
int PbDInt; | |||
void SetPrDInt(int Value) |
Look at all sections depicted in figure 21.2. The private members of the base class subobject which are accessible only through public members of the base class, in this PrBaseInt is accessible only through setPrBaseInt, however, embedded and part of the object. When PrBaseInt is part of the derived class object, the SetPrBaseInt function runs without any performance penalty when it is called by a derived class object as compared to a case where it is called by a base class subobject. The member it is trying to manipulate is available in the object itself like the base class and the time to manipulate that variable remains identical.
Private Derivation
A privately derived class copies base class public members in the subobject as private and that is why the embedded subobject now has private members, unlike the previous case. The public members of the base class now converted to private in the base class subobject. Look at the figure 21.3.
Figure 21.3 Privately derived base class subobject
Base class | Base | Base class | Derived |
object | Private | Subobject | Not accessible directly |
int PrBaseInt | int PrBaseInt | ||
Public | Private | ||
Int PbBaseInt | Int PbBaseInt | ||
Void setPrBaseInt | Void setPrBaseInt |
The private members of the base class, like the previous case, is not directly accessible now. The function setPrBaseInt is not callable by the objects, but a member function of the derived class can still call it. That is the reason why we need to have a new function SetPbPrBaseInt introduced in the derived class.
Figure 21.4 Privately derived class
Derived class structure as per C++ Object model
Not accessible directly
int PrBaseInt | Base | ||
class | |||
Private | |||
Derived | subobject | ||
Int PbBaseInt | |||
class | |||
Void setPrBaseInt | |||
object | |||
Private: | |||
int PrDerivedInt; | |||
public: |
int PbDerivedInt;
void SetPrDerivedInt(int Value)
void SetPbPrBaseInt (int Value)
Protected Inheritance
Let us brief about the protected inheritance process. It is not much different but the base class subobject is now a bit different. The public members of the base will now become protected and protected will remain the same. There is not much different either in the base class subobject structure or derived class object outline so we are not having any diagram for the same. We have already seen that the protected access specifier is not much different than private unless the class is inherited further.
In all three of the above cases, you can notice a few important things.
- The base class object is not the same as a base class subobject. The base class subobject is constructed from the base class object but with access specification based on inheritance rules and thus dependent on how the class is inherited
- There are some members in the derived class not directly accessible while some of them are, some of them are accessible only to members and some of them to objects. Compilers need to take care of all these cases at the time of compilation
- The base class subobject is there at the beginning of the derived class structure and thus other members of the derived class are to be moved below for adding them. The compiler needs to provide a mechanism for choosing the right offset for any member based on their position in the structure for accessing that element directly. The offset calculation is simple for the case that we have described above but it not that easy when we have multiple-inheritance or inheriting using virtual base classes. We will study about virtual base classes in the next module.
Compiler’s work
A compiler has to keep the track of what user has defined in a given class and how that class is inherited. An access control model is used to decide what is the access level of elements of a class to member functions of the base class, friends, member functions of the derived class as well as the objects of the base and derived class.
Access Declaration
As if the above complexity is not enough, compilers have to deal with another issue, known as access declaration. It is a facility given to the user to alter the default derivation to something else.
Let us define access declaration formally. When a typical inherited member has been introduced again in either public or protected section so as to raise it to some other access specification than it is derived with, the process is known as access declaration. Let us see how it is addressed in the C++ language.
There is a typical ‘one size does not fit all’ problem. When we want one of the members to be inherited publicly and rest of them privately, the conventional inheritance process won’t help. If we inherit using private, all will be treated as private, if we inherit using public derivation, even if we want a private derivation for a typical member (so the derived class objects should not have an access to it), we cannot do it, all members are inherited as public only. Access declaration is the solution here. Access declaration is about altering a typical inherited member’s specifier to some other value. For example, a class can be inherited privately, so all public members will be treated as private in the derived class, a derived class designer can insert an access declaration to convert one of the member’s access specifier from default private to public or protected.
Here is an example.
Program 21.2
//AccessDec.cpp
#include <iostream>
using namespace std;
class B
{
protected:
int spProInt;
public:
int spInt;
int nInt;
};
class D:private B
{
protected:
using B::spProInt;
/* now spProInt is treated as protected in D*/
public:
using B::spInt;/* Now spInt is treated as public in D */
- // B::spInt; is the old way of writing the same void SetspProIntofB()
{
spProInt = 10;
}
};
int main()
{
- D oDect;
//oDect.nInt=10;
/* Above statement, if not commented, won’t compile */
oDect.spInt=20;
oDect.SetspProIntofB();
}
Closely observe three integers. One of them (int spProInt) is protected and two of them are public (int spInt; and int nInt;) in the base class. The derived class inherits all three of them as private. That means all of them are treated as private in the derived class. However, access declaration redefines two of them under specific access specifier. The spProInt is redefined under protected and spInt is redefined under public. Look at the syntax as well.
protected:
using B::spProInt;
public:
using B::spInt;
We are redefining them under typical access specifiers and using the keyword using. The old compilers allowed access declarations without the keyword using but current compilers demand that. The keyword using inserts the member defined next as a specific type in the current namespace. In this course, we will not be discussing namespace any longer. Kindly refer to reference 1 for the detailed discussion on namespaces and the using keyword.
The statement B::spProInt; specifies as specially protected access specifier while it specifies B::spInt; as a special public specifier. Both of these members, otherwise, will be treated as private.
Access specifier has some restrictions.
- It cannot raise the status of the original access specifier. In above case B:spProInt can be raised to protected but cannot be raised to the public as in the originalspecification it is defined as protected. The B::spInt is possible to be raised to the public as it was defined as public in the original definition.
- As a consequence of above, a publicly defined member can be redefined as either protected or public while a protected member can only be redefined as protected.
The keyword using that we have used in the above program is related to namespaces. C++ defines a namespace of the inherited class which contains all variables defined inside that namespace. When we use word using it brings the variable mentioned after using that namespace. Reference 1 has an entire chapter on namespaces. One may refer to that chapter for further reading.
In fact, the compilers can alter the access specification easily because the members are already available in the object, the compiler needs to only regulate the access as per the access specifier, which is effective after the access declaration.
Inheriting further
Many times one needs to further inherit the class which is already inherited. There are generally two different cases where this is needed. One is when the user deploys a bottom-up approach is designing. For example, a designer is given a job to design classes for a university which runs Commerce, Arts, MCA, and B.Ed. courses. Once the designer set out to design classes for representing students of these branches, he might start designing classes like ArtsStud, ComStud, MCAStud, and BEdStud classes representing these students. He soon realizes that there are a few things common in these classes and it is always better to have a common class containing all common fields of these students. He might decide to have a Stud class from where he inherits all five classes. This is an example of the bottom-up design process. Here a new class is designed to inherit into an old class. The derived class comes into existence before the base class in this design. Once the derived class (and in most cases, multiple derived classes) are designed, we will have a base class as a collection of common elements.
There is one more way to have inherited classes. Whenever we need to have a specialization of a given class, we need to inherit the old class into a new class. For example, the MCAStud class might be inherited into GLSMCAStud class. This is the example of further inheritance. When we provide a specialization of a given class, it inherits from that class. For example, automatic cars are a specialization of the cars, Basmati Rice is a specialization of Rice, Punjabi Suit is a specialization of Ladies Clothing, and Patiyala is a specialization of the Punjabi Suit etc.
In case of a bottom-up design, the base class contents are defined after the derived class contents. On the contrary, the top-down design, the derived class contents are defined after base class. Both of these designs are common and a single system design process might include both of these cases.
Let us take an example to understand how further inheritance is actually carried out. Suppose we have a class describing student (Stud), a class describing MCA student (MCAStud) and a class describing an MCA Student of GLS institute, the GLSMCAStud (derived from MCAStud). A program 21.3 describes this inheritance chain. One of the most important parts which describe how the constructors are called and used. A derived class constructor needs to also build a base class object and needs content worth both classes as parameters. Similarly, a further derived class needs as many arguments as one needs for building a base, derived and further derived class. Here is the program.
//Program 21.3
//FurtherInherit.cpp
#include <iostream>
#include <string>
using namespace std;
const int Years = 3;
const int nElectives = 2;
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 MCAStud : public Stud
{
int marks[Years];
string MCANm;
string Lab;
public:
MCAStud(string t_Nm,string t_Add,char t_Grade,string t_NmSchool,int t_Marks[],string t_MCANm,string t_Lab):
Stud(t_Nm,t_Add,t_Grade,t_NmSchool)
{
for (int i=0;i<Years;i++)
marks[i]= t_Marks[i];
MCANm = t_MCANm;
Lab = t_Lab;
}
};
class GLSMCAStud: public MCAStud
{
string MCAProject;
string GuideNm;
string Electives[2];
public:
GLSMCAStud(string t_Nm,string t_Add,char t_Grade,string t_NmSchool,int t_Marks[],string t_MCANm,string
t_Lab,string t_MCAProject,string t_GuideNm,string t_Electives[])
:MCAStud(t_Nm,t_Add,t_Grade,t_NmSchool,t_Marks,t_MCANm,t_Lab)
{
MCAProject = t_MCAProject;
GuideNm = t_GuideNm;
for (int i=0;i<nElectives; i++)
Electives[i] = t_Electives[i];
}
};
int main()
{
int MCAMarks[] = {89,78,97};
string eSub[2]={“AI”,”Machile Learning”};
GLSMCAStud Jay (“Jay”, “Maninagar”, ‘A’, “Sardar” ,MCAMarks ,”GTU”, “Lab1”, “Compression”, “JD”, eSub);
}
You may derive following points form the above program.
1. An MCAStud class defines an MCA student which is a specialization of Stud class and thus inherited from it. Similarly, GLSMCAStud is a specialization of MCAStud and thus inherited from it.
2. Stud constructor has 4 arguments for 4 fields that the Stud class has. The MCAStud class has 7 arguments, 4 for constructing Stud subobject while 3 for additional members defined by MCAStud class. The GLSMCAStud needs 10 arguments, 7 for MCAStud and 3 for its own additional members.
3. Both derived class and further derived class definition includes constructors which begin with calling the respective base class constructors using MIL (member initialization list).
4. The Stud class contains information which is common for other students. If we later add ArtsStud class, that class only needs to inherit from Stud class and does not to again define those common members. Finding out fields which are common and separate them from the class by inheriting from it rather than embedding them in the class is a very important step in the design process.
5. The program does not do much of processing but it has gone on the heavy side while calling constructors. There are many fields to consider. One typical method to solve this problem is to pass a struct rather than multiple elements so the argument list becomes short.
Inheriting multiple classes from one class
We have seen a case where a single class is further inherited. Now we will look at the case where a single class is inherited into multiple classes. Look at the program 21.4. A Stud class now is inherited into an ArtsStud, MCAStud and ComStud classes. You can see how common information about the student, viz. name, address, grade, and the name of the school in which he studied are inherited from the Stud class into all four classes of our interest. Kindly also have a look at how different constructors are defined and used. You can also see how the base class constructor is initialized at the beginning of constructors of every class.
//Program 21.4
//StudInherited.cpp
#include <iostream>
#include <string>
using namespace std;
const int nBEd = 2;
const int nMCA = 3;
const int nCom = 2;
class Stud
{
protected:
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 ArtsStud : public Stud
{
int marks[nBEd];
string ArtsNm;
public:
ArtsStud(string t_Nm,string t_Add,char t_Grade,string t_NmSchool,int t_Marks[],string t_ArtsNm):
Stud(t_Nm,t_Add,t_Grade,t_NmSchool)
{
for (int i=0; i< nBEd; ++i)
{
marks[i] = t_Marks[i];
}
ArtsNm = t_ArtsNm;
}
};
class MCAStud : public Stud
{
int marks[nMCA];
string MCANm;
string Lab;
public:
MCAStud(string t_Nm,string t_Add,char t_Grade,string t_NmSchool,int t_Marks[],string t_MCANm,string t_Lab):
Stud(t_Nm,t_Add,t_Grade,t_NmSchool)
{
for (int i=0;i<nMCA;i++)
marks[i]= t_Marks[i];
MCANm = t_MCANm;
Lab = t_Lab;
}
};
class ComStud : public Stud
{
int marks[nCom];
string ComNm;
public:
ComStud(string t_Nm, string t_Add, char t_Grade, string t_NmSchool,int t_Marks[],string t_ComNm)
:Stud(t_Nm,t_Add,t_Grade,t_NmSchool)
{
for (int i=0;i<nCom;i++)
marks[i]= t_Marks[i];
ComNm = t_ComNm;
}
};
int main()
{
int ArtsMarks[]={55,75};
int MCAMarks[] = {89,78,97};
int ComMarks[] = {54,78};
ComStud Sneh(“Snehal”,”Ambavadi”,’A’,”MK Primary”, ComMarks, “HA”); MCAStud Jay(“Jay”,”Maninagar”,’A’,”Sardar”,MCAMarks,”GTU”,”Lab1″); ArtsStud Steffi(“Sonal”,”Khadia”,’B’,”Navjivan”, ArtsMarks,”Drawing”);
}
What if all these classes now need the mobile number of the student? Do we need to change at all four places? Certainly not. What we need to do is as follows. We need to add that single element to the definition of the Stud class as follows. All four student types automatically will inherit and thus have that field inside the object. Following code snippet illustrates the point.
class Stud
{
protected:
string Nm;
string Add;
char Grade;
unsigned long MobileNo;
string NmSchool;
public:
Stud(string t_Nm, string t_Add,char t_Grade, string t_NmSchool, unsigned long t_MobileNo ) {
Nm = t_Nm;
Add = t_Add;
Grade = t_Grade;
NmSchool = t_NmSchool;
MobileNo = t_MobileNo
}
};
Summary
In this module, we have seen how a class can be inherited into another class and further. We have also seen how one class can be inherited into multiple classes. Whenever the inheritance takes place, the subobjects are inserted in the inherited class based on the type of inheritance and treated as a part of the derived class. Access declaration is used for altering an inherited member’s effective access specifier. The effective access specifier cannot be raised beyond the original access specification. When inheritance takes place, the constructors of the base class are called in the derived class using MIL and they are needed for the proper initialization of the derived class object.
you can view video on Inheritance-the inside view |
References
- Programming with ANSI C++, Bhushan Trivedi, Oxford University Press
- www.stroustrup.com, homepage of Bjarne Stroustrup, the creator of C++