23 Implementing Runtime polymorphism
Bhushan Trivedi
Introduction
In this module, we will look at how virtual functions are defined and used to achieve runtime polymorphism. We will extend our discussion of the Shape class hierarchy through an example and see how that can be used to implement runtime polymorphism.
The need for a virtual function
Let us try to see where these virtual functions make sense. Consider a video game in a virtual world. The user encounters many obstacles in the path to the destination and decides the way to move ahead. The obstacles are designed through a program using various combinations of basic shapes like triangle, rectangle, and circle etc. The obstacles are constructed by placing these basic shapes at various places in the given frame. Once such frames are emphasized, they are moved across so users feel that the obstacles are moving towards him. Such a program demands many components but one such component is to construct such obstacles based on basic shapes defined. We have one such program defined in 25.1. The program shows how virtual functions can be used in such circumstances. We have a hierarchy defined with the shape being a base class. Pt is a class which defines a point. All other types of figures inherited from Shape contain some points and some other attributes. For example, we have a circle, in which we have an R or radius as well as a center
// Program 24.1 block 1
//UsingVirtualFunction.cpp
#include <iostream>
#include <ctime>
using namespace std;
class Frame;
class Pt
{
int X;
int Y;
public:
Pt(int t_X=0, int t_Y=0)
{
- X = t_X;
- Y = t_Y;
}
int get_X() const
{
return X;
}
int get_Y() const
{
return Y;
}
friend ostream & operator <<(ostream & t_Out, Pt & t_Pt);
};
ostream & operator <<(ostream & t_Out, Pt & t_Pt)
{
t_Out << “( ” << t_Pt.get_X() << “, “<< t_Pt.get_Y() << ” )”; return t_Out;
}
as a point. Similarly, when we have square, we have Len or length as the int and a left bottom as the point. What we have assumed is that a figure is drawn at a specific point and with a specific attribute. We have not provided the code for actually drawing that figure. However, that is not needed for showing the need for a virtual function in that program.
//Program 25.1 block 2
class Shape
{
Pt P;
int Color;
virtual void draw()
{
cout << “It is Shape”;
};
friend Frame;
};
class Square:public Shape
{
Pt l_bottom;
int Len;
public:
Square(Pt t_L_Bottom, int t_Len)
{
l_bottom = t_L_Bottom;
Len = t_Len;
}
void draw()
{
cout << “Square at”<< l_bottom << ” L =” << Len <<
“\n”;
};
};
class Triangle:public Shape
{
Pt A, B, C;
public:
Triangle(Pt t_A, Pt t_B, Pt t_C)
{
- A = t_A; B = t_B; C = t_C;
}
void draw()
{
cout << “Triangle at “<< A << ” ” << B << ” “<< C <<
“\n”;
};
};
class Circle : public Shape
{
Pt Center;
int R;
public:
Circle(Pt t_Center, int t_R)
{
Center = t_Center;
- R = t_R;}
void draw()
{
cout << “Circle at “<< Center << ” R = ” << R << “\n”;
};
};
Before we look at the code of the program, let us try to understand the structure and the need for using the virtual function in this case. Every frame that we introduce has some random images. The images are generated using the rand () function. Based on rand () function output between 0 and 2, either circle, square or triangle is decided to be drawn. Another rand () function also decides the values of other attributes like center point and length etc. Interestingly, the current time is obtained using a time () function and used that as a seed value. That means the rand () function which produces pseudo random values, generate a different sequence of values every time this program runs. This set of total 25 random images combines together into a frame. The draw () function draws whatever image that comes in the array, irrespective of the type. The array, being filled with random images, have no preconceived structure. However, we do not really want to know what is the content of the array item, whatever it is, we just draw it. The draw function call itself decides what to draw. defined afterward. The Frame deals with elements of Shape and that is why Shape demands class frame to be a friend. To help the compiler learn about the class frame, forward declaration is provided just before the definition of the class Pt. The point class contains X and Y coordinates as int, a constructor with both X and Y values input, a get_X function for getting X coordinate while get_Y function for getting Y coordinate. The << operator is overloaded for displaying the point in the form of (X, Y).
The program begins with the definition of class Pt which defines the most basic element of the figures. The class Shape and respective hierarchy are
//Program 25.1 block 3
class Frame
{
Shape * Images[25];
public:
Frame()
{
srand( (unsigned)time( NULL ) );
for(int i=0;i<25;++i)
{
int rVal [6];
for (int j = 0; j<6; ++j)
{
rVal[j]= rand() % 50 ;
}
Pt P1(rVal[0], rVal[1]);
Pt P2(rVal[2], rVal[3]);
Pt P3(rVal[4], rVal[5]);
switch(int choice = rand() % 3)
{
case 0:
Images[i] = new Triangle(P1, P2, P3);
break;
case 1:
{
int Len = rand() % 20;
Images[i] = new Square(P1,Len);
break;
}
case 2:
{
int R = rand() % 10;
Images[i] = new Circle(P1 , R);
break;
}
default:
cout << choice << ” control shouldn’t come here! “;
}
}
}
void draw()
{
for (int i=0; i<25; ++i)
{
Images[i]->draw();
}
}
};
Now it is the time to look at the hierarchy in block 2. The class Shape has two data members, point P, and int value color. The most important component is the virtual function draw (). Frame class is made a friend because it accesses the private members of this class.
We have the Square class defined next. It contains two data members, a point describing the left bottom of the square and the length. One can use these two values to draw a square at a specific position in the frame. We also have a constructor for this class which constructs the square based on the left bottom point value and the length of the side. We also have a function draw () which is, by default, overloads the base class Shape’s virtual draw() function and thus virtual by default.
Next class is the triangle. It has three vertices (as points) named as A, B and C. It also has a constructor which, based on these three vertices values, constructs a triangle. Class triangle also has a draw () function which is again by default virtual and draws a triangle.
The final class in the hierarchy is Circle class. It has two data members, a point as a center and a radius R. It also has a constructor and a virtual draw function for drawing the circle.
You might be surprised as there is no code for actually drawing any object. You may also be surprised that C++, in its original form, does not have a graphics library. Most C++ compilers, though, have some form of the graphics library, based on the OS they are running on. Graphics libraries need to run faster and thus are usually tied to the hardware and the OS they run on. That means it is hard to write a general purpose library to include in the C++ standard. We have avoided drawing the figures for two reasons, first is, obviously, we do not want our code to be bound to a typical compiler and OS, and second, it is not needed. Our current job is to learn how virtual functions can be used and in which situation they make sense. The example that we study suffices the purpose. You can assume the figure is actually drawn using specific functions provided by specific libraries used by the compiler that you prefer to use.
Now, let us concentrate on the final class that we have in our example, the Frame class. The class is not inherited from shape but contains 25 different shapes in a form of an array of 25 shape pointers. This is a classic method of using such hierarchical objects in the memory. We do not really know which pointer points to which object and thus we do not have a prior knowledge about their sizes. An array of pointers solves the first part of the problem. As soon as a new image arrives, or decided, new is called to get the required memory and the pointer is made to point to that object, problem solved.
The default constructor of the frame object is designed to do the job of generating a collection of a random set of images. It starts with taking a seed from the time function and applying srand to set the seed value. Based on that seed value, the rand () function is used to generate 6 different points. Another call to rand decides value between 0 to 2 and based on that a random image is chosen. It also invites a dynamic allocation using new to have a
proper memory for that object and then construct that object. Parameters like radius or length are also decided based on another call to the rand () function. All in all, this process serves to provide a random collection of 25 different images in the frame object.
int main
{
Frame_Tframe
Tframe.draw()
}
Finally, we have the unequivocal main () where a single copy of the frame object is defined and a draw function of that frame is called. Is this draw () a virtual function? No. The frame is not inherited from Shape and thus this draw (), even when bearing the same name as the virtual function defined int eh shape class and also the same prototype, is not a virtual function. It is not even an overloaded function; it is simply a different function of a different class bearing the same name. The draw function of the frame object does a simple job of picking up each of the objects from the array of images and calls their virtual draw function. Based on the type of object, specific draw function is called and the frame is generated.
OO programming methodology
The method of programming that we have used in above example is known as object oriented programming. It demands a few things from the programmer to design,
- We need to have a base class with at least one virtual function defined, one can have this function with an empty body. It is just fine.
- We also need to have a hierarchy of classes inherited from that base class, based on the designer’s model for a problem that he or she is trying solve.
- We also need to have a pointer pointing to the base class and as and when a new object of ANY class inheriting from it is needed, that pointer is used to get one using new operator.
- The very pointer is used to access the objects in a flexible manner1.
- The code is written without considering a specific object. Any specific requirement of the object (for example square demands setting the length), is coded in either virtual function or member functions. Common requirements like choosing the random values etc. is done in a generic form outside the virtual function. Virtual functions are designed to do specific things for that class which demands different treatment than other classes, to provide a consistent interface to the programmer who is using it. The object is always used as dereferenced pointer (*pShape) so as to work with polymorphic types of objects.
- The generic code is so flexible that it works on ANY type of object and special part of those objects are handled using virtual functions, making the program work without any assumption about underlying object being manipulated.
The amount of flexibility achieved by OO programming is tremendous. Proper design of classes and virtual functions hides the differences inherent in the objects internal part and help programmer to design a sustainable and extendable code. This part needs more explanation.
- 1 The C programmers used to use void pointers to similar effect but the virtual function is better as it can take decision at runtime. Also, having an explicit language support simplifies the code and makes it easier to debug. Consider we have another class called hexagon added in the hierarchy, what exactly we need to do for changing the frame class? Not much except for providing one more case in the switch statement. Even that case can be eliminated with little clever coding. Remaining part, including calling draw () in the main loop, remains as it is. Even when one of the class is removed from the hierarchy, the main part does not change much. Whenever a programmer wants to add a class to a hierarchy, he only needs to take care of three things,
- Keep all generic things dependent on the base class, and so inherit from it.
- Keep all object dependent jobs described in member functions (for example the constructors for constructing that object)
- Keep all jobs which the programmer wants to remain consistent with other objects but to be done in a different fashion (for example drawing the object) in virtual functions.
Once all of the above is done, the programmer can avail all the benefits of the flexibility provided by the OO programming. On the other hand, OO programming demands two things which other types of programming does not.
- OO programming requires additional overhead. We have thrown some light on this in this module but last three modules of this course provide much better insight on those issues.
- OO programming also demands proper design of the system. Thinking about the complete hierarchy, segregating common functions as virtual functions or provide the right mix of member functions for all classes demands a proper visibility. In this course, we will not be stressing on the design part but it is really important for a working code.
Let us take one example to illustrate the second point. Consider a news channel receiving news from various reporters across the country. Reporters send the information in form of a news item. There are many different types of news, some of them have images, some have videos, some have text and so on. They also have a length associated with as well as many other parameters including the type of news (political, sports, crime, Bollywood related etc.), criticality of it, and so on. The automated program has to select top 20 news for example and provide them to the user based on his choice. The program design needs a lot of flexibility, proper hierarchy design and host of virtual functions for choosing, processing and displaying those news items. You may have a news item as a base class and derive that into multiple types of news. In modules 16 and 17 we have thrown some light on object based programming. Often a programmer has to choose between either of these models. Object based is quite generic but OO is better in the sense that it is more flexible and can take decisions at runtime. The price that it has to pay is additional runtime overhead. A programmer can carefully design a system with a combination of two different methodologies which makes it even harder to design that system. We will not discuss that issue further in this course. Two excellent resources on how C++ code can be better designed are two books from Scot Mayer, Effective C++, and More Effective C++. You may read them for the further insight of this part.
Need for virtual destructors
The program that we have seen with virtual functions seems great unless you find something has skipped your eye. We allocate memory for those 25 components but we have no provision for deleting them. In our case, the part that we are addressing is the program and once the program completes, the destructor must bear the same name as that of the class preceded by the ~ sign. So, what is the point in making them virtual in this fashion?
This is the only virtual function which has different names in the base and derived class. However, the behavior is also little different. Let us try to understand the need for a virtual destructor first. Closely observe the Frame destructor. It executes a single statement for deleting any Image, delete Images[i];
The image is a pointer to the Shape class and deletes in sub object destructor case only removes the memory of the Shape subobject of the image but when we have defined that destructor as virtual, it provides the delete operation of that specific object and releases the complete memory of that object. This is the advantage of having a virtual destructor. One can use a base class pointer to release the memory of any derived object pointed to by that pointer.
The virtual destructors are little different than other types of virtual functions a bit. In above case, a virtual destructor to Circle also calls the virtual destructor for Shape, why? Because the circle object contains a shape sub object. In other types of virtual functions, the derived class function is called in place of the base class function.
Let us summarize the understanding of the virtual destructors. A virtual destructor is a normal destructor with a virtual keyword preceding the name. When a pointer to a base class object is used to access the derived class object, calling a virtual destructor with such a pointer releases the memory belongs to the derived class object and not only the base class subobject. Unlike conventional virtual functions, a virtual destructor of the derived class object also invokes the virtual destructor of the base class subobject.
Pure virtual functions
We have discussed that designing functions and class hierarchy which can provide complete flexibility of doing exactly what user wants in the best possible manner demands good system design. During the design of the virtual functions, it is possible that there are two different types of job needed to be done by the programmer. In case 1, the programmer wants following.
- There is a method to do something, the programmer defines that method in the base class. For example, some method to handle errors. The programmer also wants all classes in the hierarchy to have some method to handle errors. An inherited class, if feels that the base class method to handle an error is fine and nothing more to be done, can always fall back to it.
- There is a method to call a function using a typical set of parameters and make sure that all derived class must implement that function with the same set of parameters without fail. The inherited class may have their own methods to implement but they cannot work without having their own method. The functions of all such classes have a similar set of arguments2.
For implementing the first type of method, virtual functions are used. If a base class has a virtual function, it can inherit some classes with or without a virtual function. When an inherited class has a virtual function of its own, it overrides the default behavior which is provided in the base class, when the class does not feel necessary to do so, the base class function is used instead.
Many times, we need the second type of functionality. For that, C++ has provided pure virtual functions. They are defined in little different fashion. For example, in the case of the hierarchy that we have just defined, the draw function of the Shape class can also be defined as follows.
virtual void draw () = 0;
Kindly note that there is no body of the draw () function now. One can define that body outside the class now as follows, however, it is not compulsory and not really done in most cases.
void Shape :: draw () { cout << “Shape “; }
There are a few consequences of defining a pure virtual function in a class. Here is a list.
- The class cannot have objects now; the class is called an abstract class3. This is a good move for most base classes are abstract entities in the real world. For example, in a given frame, we might have a square, a circle or a triangle but no shape.
- The inherited classes, if they have objects, must implement this draw () function exactly with the same prototype as of the base class. This also is a better move as every figure inherited from shape must provide its own draw function. It cannot omit the definition and fall back to base class function now. In a way, defining a pure virtual function instead of the virtual function tells the derived class “define your own version as per my prototype”
Looking at this description, you may feel that defining a draw () function as pure virtual makes more sense as we do not want inherited figures to fall back to Shape’s draw.
2 Similar construct is known as an interface in Java.
3 Some other languages like Java has explicit provision to name classes as abstract to force not to have objects.
to display that message indicating that the error is general. The SyntaxErr and LogicalErr are derived from it and having virtual function
Solution implemented. We also have CompileErr and LinkErr inherited from SyntaxErr while LoopingErr and InitErr from LogicalErr.
Now concentrate on main(), it defines two pointers to err, pError and qError which points to InitErr and LinkErr objects respectively.
When statements like following are encountered, the output executes their Solution function or the base class function.
pError->Solution();
qError->Solution();
observe will print following where the LinkErr’s Solution function is not commented.
Init
Linker
When it is commented as shown, it prints following instead.
Init
Syntax
Now you can understand why virtual functions make more sense here.
Can you see what is the significance of a non-virtual function? For example, we have a function which returns an error code for any error defined inside the Err class as follows.
int returnErr() {return ErrCode;}
what is the significance of such a function?
You can see that this function, being a non-virtual, inherited by all classes of the hierarchy as is. A derived class should not define a function with the same name as one of the critical principles of a good design. That means every inherited object can always call returnErr() which is called in the same fashion throughout in the hierarchy, without any change! That means, a class design should have non-virtual, virtual and pure virtual functions based on the need and expected usage of the class, the point that we are continuously trying to emphasize.
Summary
In this module, we have shown how virtual functions are possible to be implemented using a subset of hierarchy that we have introduced in the previous module. We have also seen what are the consequences of defining a virtual function. We stressed on the need of proper design to take advantage of the flexibility provided by OO programming. Finally, we have seen a pure virtual function which compels the inherited classes to implement the pure virtual function in exactly the same format as provided by the base class.