34 How Object Model Changes the program
Bhushan Trivedi
Introduction
In this concluding module, we will look at how the object model changes the C++ program. We will look at copy constructors in the beginning. We will see what exactly is changed when the function return process is optimized by the compiler. We will look at why the C++ designers decided to embed the inherited members into the object and finally we will see an example which describes how the C++ object model changes the program. C++ object code modifies the object definitions for the inclusion of vptr and vtble like additional members. It also modifies the function calls to match with requirements. It modifies the assignments based on the hierarchy to remain consistent with user’s expectations and virtual inheritance. It adjusts the pointer values to return right type of object while assignment or other operations are carried out, especially in the case of virtual inheritance and multiple inheritances. It adjusts the code as per static or dynamic calls made by the program. For static calls, it resets the vptr to the class it belongs to, while for dynamic calls it provides vptr pointing to a specific entry in vtble.
Copy Constructors
Like default constructors, copy constructors, when not provided by the programmer, is constructed by the compiler, when needed. The copy constructor is invoked in three different cases; explicit initialization, passing arguments to a function and returning it back1.
If the programmer does not define a copy constructor, a compiler must provide one in these three cases. Compiler normally provides a member wise copy when a class object is copied. Let us take example depicted in figure 36.1.
class MyDetails {
int weight;
int height;
int AdharNo;
int LicenceNo;
public:
…..
};
MyDetails Ganesh (250,6, 1234567, 345678); MyDetails Kartikeya(Ganesh);
The initialization of Kartikeya with the values of Ganesh is done using a copy constructor. Three members of the MyDetails class, weight, height, AdharNo, and LicenseNo are all copied from object Ganesh to object Kartikeya.
Kartikeya with the values of Ganesh is done using a copy constructor. Three members of the Deity class, Address, Father’s Name and Mother’s Name are all copied from object Ganesh to object Kartikeya.
In this case, the Kartikeya.Address is initialized with string object “Kailash”, Kartikeya.FatherName is initialized with value “Shankar” and Kartikeya.MotherName is initialized with value “Parvati”. This process is known as member wise copy. Every member from the Ganesh object is copied to the Kartikeya object using a string class copy constructor.
Unlike the previous case, a bitwise copy cannot be done as each member is an object itself and string class which they belong to, have its own copy constructor defined. That copy constructor must be invoked and thus the member wise copy cannot be reduced to bitwise copy in this case. let us recap both bitwise and memberwise copy before proceeding further.
Memberwise copy: When an object is copied into another object, every member of the object is copied recursively (because members may themselves be objects which have their own members) into another object, this process is known as a memberwise copy. This process is slow as it needs to pick up each member and copy each member’s members if it is so.
Bitwise copy: When an object is copied bit by bit, like a C struct being copied into another, is known as a bitwise copy. This is a faster operation as the bits are copied blindly without any check.
Interestingly, it is also possible to have a class object embedded in another object. For example, the face object that we have seen before once again is depicted in figure 36.3.
Bitwise copy cannot be done for copying Ganesh object into Kartikey if the classes like Circle, Triangle, and Square have their copy constructors. In fact, all these classes also have objects of type Point. Thus the member wise copy operation is called recursively for each member and member’s members in The copy constructor, if not provided by the user,must be Figure 36.3 embedded objects demand constructed by the compiler if the members cannot be copy constructor synthesizing copied using bitwise copy. The member wise copy is performed using the compiler supplied copy constructors
- 2 When two struct values, copied from one to another, copied bit by bit.
For example, in above two cases, compiler supplied constructors may be defined as shown in figure 36.4. The copy constructors for them also have copy constructor for the Point object It is clear from our earlier discussion that a class with a virtual function always contain a vptr. When an object is copied into another, it is not a good idea to copy the vptr into a copied object. Why? It is quite possible that an object being copied is a derived class object while the object to which it is copied to is a base class object. In that case, the vptr of the base class is to be set in the target object and not one which is provided in the source object. Let us take an example to understand in figure
Student *pStudent; MCAStudent MS (…);
Student S = MS;
The base class object S is assigned a value which is a derived class object MS. The derived class object is sliced and only base class subobject of MS will be assigned to S. however if the vptr is copied as it is, we have a problem when we encounter a statement of the following type, it will incorrectly call the function of the derived class and not the base class Compiler, while trying to avoid this problem, synthesizes a copy constructor with the following entry.
S.__vptr = MS.Student::__vptr;
That means the vptr of the Student subobject of the MS object is copied into vptr of S3. Look at figure 36.6. The MS’s vptr value and the student object’s vptr value is different. The normal memberwise copy will assign the vptr of MS to S, which is wrong and thus a copy constructor needs to the synthesized
The final case, obviously, involves the virtual base class. When the object of a derived class is assigned to a base class, for example, following code is available where B derived virtually into DD using virtual base class inheritance using two classes D1 and D2. The d2 contains two parts, a subobject of class B as well as a subobject of class D2. Getting both of them from the object dd, which contains data as shown in figure 35.15, requires two operations, extracting both sub objects and present them together as one. The compiler needs to intervene and do it.may encounter another case as depicted in figure 36.8. assigned the value of dd, the compiler can safely copy bit by no need to change any values. This is because D1 is the first class. In a case of d2, it is an object of the second virtual
For every virtual base class which is not first, the compiler intervene and add required code. It has to set the virtual
- 3 This is a case when the virtual function like behavior is not exhibited by the compiler because we are dealing with objects and not a pointer to objects. S is a student object and it only has an access to the getMarks() function associated with that class while MS is an MCAStudent object and it has the access to getMarks() of its own class.
base class pointer to point to a typical set of virtual base class subobjects.
Optimization while returning
Compilers not only change the code to keep the code consistent and correct but also for optimization. We will discuss one of the common issues of optimization, the process of returning something from a function.
Suppose we have a class called Time at our disposal. A long member of the Time class, TimeInSeconds, returns time elapsed so far in seconds. We are asked to write code to compare two Time objects. We may write a function Later which compares two Time objects based on TimeInSeconds value and return the time with a later value.
In fact, we present three different ways to write the same function in 36.9. Kindly observe the code and understand why the third version is little better than the earlier ones.In the case of the first version of the function, there are a few initialization and temporary object creation needed For example, if we invoke the Later function based on two Time values, Time1 and Time2, 36.10 depicts the code
Time Later (Time T1, Time T2)
{
if (T1.TimeInSeconds) > (T2.TimeInSeconds)
return T1;
else
return T2;
}
Time & Later (Time T1, Time T2)
{
if (T1.TimeInSeconds) > (T2.TimeInSeconds)
return T1;
else
return T2;
}
Time & Later (const Time & T1, const Time & T2)
{
if (T1.TimeInSeconds) > (T2.TimeInSeconds)
return T1;
else
return T2;
}
Time Time1, Time2, LaterTime;
…
LaterTime = Later (Time1, Time2);
This code has to change to something similar to figure 36.11.
Time Time1, Time2, LaterTime; // This statement does not change
…
Time TempTime1(Time1), TempTime2(Time2);// Temporary variables initialized
Temp = new Time (); // Temp defined for a return
Later (TempTime1, TempTime2, Temp); //Third argument of type Time & is
LaterTime = Temp; // added by the compiler.
delete Temp;
Why two temporaries TempTIme1 and TempTime2 are defined? It is done to keep the value parameter passing semantics to work properly. Even if the Later function changes the values of the arguments, they are reflected back only in the temporaries and not in original variables.The third temporary variable Temp is an interesting addition to the list. The Later function is modified to accept the third argument as shown in 36.11 and the code is also changed to use that argument.
Later (Time T1, Time T2, Time & Result) {
if (T1.TimeInSeconds) > (T2.TimeInSeconds)
Result = T1;
else
Result = T2;
return Result;
}
In the second version, we are saved off from introducing Temp as we are indicating a reference being returned. Thus the statements change to as mentioned in figure 36.12. Three statements are now removed and underlined part is changed. The function Later is not changed, except for no explicit return statement needed now. Instead of temporary variable Temp, we are using the actual variable now. In the third version, even the first two temporaries are not needed, the actual variables themselves are passed to the function as a reference. The only problem is when the reference to the actual variables are passed, and if the function inadvertently changes their values, they are reflected in the calling program. We can avoid that side effect by preceding
both arguments by const. Also, note that Result does not require an explicit return statement now.
If you look at the code of the earlier versions, you can think that even if the programmer does not do it explicitly, can the compiler do it? It will substantially improve the performance especially when the arguments being passed are big objects. The big copy reduced to only a copy of pointer values and thus speeds up the process. Also, when the temporaries are used, the copy operation happens twice, first on temporaries and then from temporaries to the actual variables. One of the copy operation is also removed now. A compiler can and do convert code written like version 1 into version 3. That process is known as Name Return Value Optimization or NRV optimization. It is provided by all compilers.
What are the additions if the compiler offers NRV?
- An additional argument of the object which is to be returned is provided. This additional argument is of type Time & in our case and of type <someclass> & when the class is <someclass>
- Copy constructors are invoked if there are local variables which are returned. In our case, we do not have one. We can easily change the code to have one as follows. The local variable Temp is used in the processing and at the end, the value is passed to the result via a copy constructor. 36.15 describes such code.
Later (const Time & T1, const Time & T2, Time & Result) { Time Temp;
Temp.Time::Time(); // invoking a default constructor if (T1.TimeInSeconds) > (T2.TimeInSeconds)
Temp = T1;
else
Temp = T2;
Result.Time::Time(Temp); //invoking copy constructor
}
Figure 36.15 When copy constructors are needed
ADT or hierarchy
Sometimes, a programmer is in dilemma, either to go for a single class describing everything needed under one roof or design multiple classes in a hierarchical form. Consider the case of the hierarchy of Student, MCAStudent and GLSMCAStudent classes. It is also possible to define the GLSMCAStudent class as an ADT as follows Class GLSMCAStudent { <all members that otherwise will be part of the earlier GLSMCAStudent class>, including function members and data members}
will be a better method to code? The real answer is, it depends. It depends on the situation, the demand of the user, future extension required and so on. The inheritance is a great tool if we expect the system to evolve further. In above case, it is really better to go for the first version if we expect other types of students to join the system and other MCA students (other than GLS) also are likely to be part of the system.
However, our interest is to find out if the first version of the solution has more overhead than the second one. Ideally, there should be none. The C++ compilers, when implementing the hierarchy like above, embeds the base class object into a derived class object and thus both the inherited version of GLSMCAStudent and ADT version of GLSMCAStudent have almost identical structure and thus there is no performance overhead of the hierarchy. Let us look at a simple program to showcase the point. Look at figure 36.16.
The derived class has just one member, an integer which, according to the compiler that I choose, uses 4 bytes. However, it shows the size as 8, indicating that the derived object contains a base class subobject, which is 4 bytes. This clearly indicates the embedded model that we discussed is implemented by the compiler that is being usedSometimes the hierarchy has some overhead, for example, when the virtual base class is used or virtual function is used. However, that is because such hierarchy provides services which the ADT like objects cannot provide. For example, we might have a virtual function called CurrentLocaion() in the student class. When this function is invoked by the specific object, the function mightreturn the current location of the student, based on some run time information of that very class. For example, MCA students seat in building A, and if a student belongs to that class, the current location reveals that information. Such a facility is not possible in C-Struct like ADT.
A C++ programmer has informed and chosen overheads to deal with rather than default and hidden overheads. The C++ design is primarily oriented towards optimizing the performance, sometimes at the cost of user friendliness or flexibility. In above example, the crude method of embedding the base object in the derived object demands seemingly unnecessary additional memory demand. One would argue that a pointer to a base object
would be a better solution; as it saves substantial memory when the base class object is bulky and there are many inherited objects around. Having said that, such a design adds a runtime performance overhead. Whenever a derived class object is accessed, for example, look at the following code, it demands the system to traverse the pointer to access the base class sub object and its value. This base class subobject, if not embedded, might take an inordinate time to traverse the link and access the member value Name from that sub object.
GLSMCAStudent Ganesh;
cout<< Ganesh.Name; // a base class member
The name is not a member of the derived class but a base class. Suppose if a derived class has a member called ClassTeacher, another statement cout<< Ganesh.ClassTeacher will be now part of the object and thus directly addressable, unlike Name. Fetching Ganesh.ClassTeacher, in that case, will take lesser time than Ganesh.Name. In fact, in our case, we have a chain of three objects only. What if the inheritance chain is longer? Ganesh.Name will take even longer time, depending on the number of pointers it has to traverse. Such a discrimination is not preferred by the C++ designers and thus the crude method of embedding the base class object into derived class is chosen. In this way, we can have similar performance for ADT or hierarchy based solutions.
Even when virtual functions are used, most C++ compilers are smart enough to resolve the call at compile time. For example, if we have a call to CurrentLocation()
in the case depicted in 36.17. GLSMCAStudent Ganesh GLSMCAStudent *pGanesh; …..
pGanesh = *Ganesh; pGanesh->CurrentLocation();
The call to the function CurrentLocation() does not need dynamic information at runtime as it is known to be pointing to Ganesh at compile time itself. Thus such calls can be resolved at compile time. The C++ object model is smart enough to resolve only necessary calls at runtime and all others at compile time. Consider the code described in figure 36.18 based on my book on C++ mentioned in reference-3. In that example, the function draw () is defined as virtual. Consider the following statement
PtrShape->draw()
This call fetches the correct function based on the object PtrShape is pointing to. How C++ provides that functionality is the important part we are trying to address. Two things are done to address this problem. First, a virtual table for both Shape and Circle classes are created. Our concern in the Circle class right now. Virtual Table for Circle is mentioned as __vtble_Circle in our diagram 36.19. Second, every object of the class additionally contains a vptr pointer(__vptr__). For example, when a statement like following is encountered, apart from data members of the Circle class, additional Ring::__vptr is also inserted in the object.
Figure 36.18 a hierarchy and pointer to an inherited object
Circle Ring;
Ring::__vptr__ = &__vtble__Circle //statement added by compiler
This pointer, Ring::__vptr__, is made to point to the virtual table of the Circle class. The circle class definition now will have an additional statement inserted in the body of the constructor. Indicating the second entry of the table is a pointer to draw () function of the Circle class.
__vtble__Circle[1] -> Circle :: draw()
When PtrShape is made to point to a (temporary) object generated by statement like following,
PtrShape-> new Circle;
// PtrShape->__vptr__ = & __vtble__Circle
Thus PtrShape->__vptr__ is made to point to __vtble__Circle.Let us assume that a new class Rectangle is also defined. Let us make PtrShape pointing to an object of that class by following statement.
PtrShape -> new Rectangle;
// PtrShape -> __vptr__ = & __vtble__Rectangle
This will make the PtrShape -> __vptr__ to point to __vtble__Rectangle, that means when we have the following call statement
PtrShape -> draw (),
The call statement may be translated into following by compiler.
*PtrShape->__vptr__ [1] // this will call the specific draw() function
The index value 1 is fixed index of draw () function by whatever class inherits it and thus it solves the problem of accessing the function at runtime. Another point is also worth noting. A typeinfo object, which indicates the type of the object for the RTTI (Real Time Type Information, a process by which a user can determine the type of the polymorphic object before start working on it) is also made part of the virtual table. Though the RTTI has not much to do with virtual functions it is used to save space. That means this vptr and vtble are present even when the compiler does not encounter any virtual function in the class but the RTTI is ENABLED4.
Another point that you might have noticed is that I have mentioned a temporary object. The statement PtrShape-> new Circle; actually generates a new object (of type Circle) which we have not defined. A compiler adds additional code to deal with such temporary objects. For
- 4 RTTI invites a lot of additional runtime overhead and thus kept as disabled by default. If a programmer wants to use it, he will have to enable it. more details, you may refer to Reference-3 or for detailed discussion refer please refer to Reference-1. Object model changes the program Compilers, while compiling a C++ (or any other language) code, performs two operations.
First, they modify some statements for efficiency and execution convenience and two, they add some statements and objects for their own working. Consider following contrived function definition as a part of a class Student in figure 36.20. It is contrived to show how object model changes the program.
Note: – In all three modules we have shown some code snippets which indicate the program is changed to another C++ program. In the actual case, the compiler may generate an intermediary in little different manner, might generate intermediary in its own preferred language. Many compilers generate assembly code of the similar type.
Student MarksheetCalculate()
{
Student S;
Student *pStudent = new Student;
S.getMarks();
//getMarks() is a virtual function, this is static invocation pStudent->getMarks();
// this is dynamic invocation of the virtual function delete pStudent;
return S;
}
Figure 36.20 The user written program
One of the possible conversion to the object model of the above code is described in the figure 36.21.
void MarksheetCalcualte (Student & _TempStudent)
{
__TempStudent.Student::Student();
if (pStudent = _new(sizeof (Student))) != 0 ) pStudent->Student::Student();
getMarks(& __TempStudent);
(*pStudent ->__vptr__[2]) (pStudent)
// getmarks() is 3rd entry in vtble)
if (pStudent !=0 ) {
(*pStudent->__vptr__[1]) (pStudent)
}
- // 2nd entry in vtble is destructor) __delete(pStudent)
Figure 36.21 Compiler modified code
Summary
In this module, we have seen how the C++ object model changes the program to make sure promises for the efficiency is maintained. We looked at three things, the copy constructor, hierarchy based on virtual functions and NRV optimization. We have observed why the method of embedding a base class sub object within a derived class, however memory inefficient it can be, makes sense as it provides performance equivalent to a similar ADT design.
References
Reference 1: Inside the C++ Object Model Stanley Lippmann, Addition Wesley
Reference2: www.stroustrup.com, homepage of Bjarne Stroustrup, the creator of C++
Reference 3: Introduction to ANSI C++, Bhushan Trivedi, Oxford University Press