More Polymorphism (Poly polymorphism!)


Students generally aren’t able to appreciate the use of polymorphism in programming. So let’s revisit this topic. We’ll start with the problem, assuming that we don’t have inheritance in C++:

Let’s say that we are creating a game similar to Microsoft’s Age of Empires. The concept of the game is that a player should build an empire and destroy all other empires. An empire consists of different characters/units - villagers, archers, horsemen, cavalry etc. Villagers are needed to collect resources and resources (like food, wood etc.) are needed to create units. Any game will run within a window (a gaming window - i.e. the entire game program will run within this window).

Our requirement is that if at any point of time, the player minimizes the gaming window, then the game should pause, the window should minimize into the taskbar, the desktop should appear and the user can work on other things. To resume the game, the user can click on the taskbar. On clicking, the gaming window should maximize and all the characters which were present at the time of minimizing should be redrawn on the screen.

Initially while developing the game we decide to work with only two types of characters: villagers and archers. So we would design two classes to represent them. In this section we shall concentrate on our requirement (which is redrawing the characters on the screen as they were at the time of minimizing the window; let’s ignore other aspects of the game). We should provide a draw( ) member function in each of these classes (for drawing the villager or archer on screen; the draw( ) function varies in the two classes because villagers and archers are represented differently in our game).

Every villager and archer created in the course of the game has to be contained within the gaming window (the player can just click on a villager icon to create a new villager and on an archer icon to create a new archer). If the player creates 10 villagers then we should draw these 10 villagers on the screen. The draw( ) function will serve this purpose.

Our gaming window will be another class (all objects created in the game, have to be placed within this gaming window; the window will have properties like height, width etc.; the window can be minimized, restored and closed - these would be some of the member functions of this window class).

Our window class also needs to keep track of the various villager and archer objects created. Thus to store these objects, we can create an array to hold villagers and another one to hold archers (in reality we won’t use arrays since they are of a fixed size and we can’t predict how many villagers or archers would be created. We’ll take a look at different data structures in a later chapter but for the time being let’s assume we use arrays for this purpose).

The reason we need to keep track of the objects created is simple; when the restore( ) function is invoked (it gets invoked when the user restores the gaming screen after it has been minimized), all the objects present in the window need to be redrawn. When the gaming window is minimized and then restored it, we would need to redraw the villagers again. If there were 5 archers then these would also have to be redrawn within the gaming window.

So each time a new villager or archer is created we’ll store the address of the new object in our villager or archer array (i.e. we’ll store a pointer; we can also have an array to store the archer and villager objects itself but this would lead to presence of multiple copies of our object in memory- so instead of the object we store the address of the object in the arrays). In our restore( ) function we will cycle through each of these two arrays; in each cycle we’ll retrieve the address of the object and invoke the draw( ) function for that object. In this way we’d redraw all the characters which were present on the screen just before the player minimized the window.

Note: The villager and archer class would have many other properties (like location: x and y coordinates, which will be required to draw the object at the correct position) but to keep the example simple we won’t consider them.

Our restore function might be something like this:

void restore( )
{

Perform till end of villager array
{
    varr[i]->draw( );
}

Perform till end of archer array
{
aarr[i]->draw( );
}

}

where ‘varr’ is the array containing the addresses of all the villager objects and ‘aarr’ is the array containing the addresses of all the archer objects (instead of the address we could also store the entire object in the array but this would consume a lot of space).

To populate the villager and archer array we’ll need to have another two functions in the window class:

add_archer(archer *);
add_villager(villager *);

Every time a new villager or archer is created we’ll call the appropriate function to populate the ‘varr’ or ‘aarr’ arrays.

The program flow seems satisfactory:

  1. User can create archers or villagers.
  2. If an archer is created then the function add_villager is called to add the address of the new villager object to the array ‘varr’.
  3. Similarly if a villager is created then add_archer is called and ‘aarr’ is populated.
  4. In both cases their respective draw( ) functions are called to draw the character on the screen.
  5. If the player minimizes the window and then restores it, the restore( ) function of the class ‘window’ would be called. This in turn will cycle through both arrays and call the appropriate draw( ) functions.

Everything is fine except for the problems we’ll land into later.

1.) Both archers and villagers have some properties in common (example: life). We are replicating the same chunk of code repeatedly.

2.) If we were to add a new character, say swordsmen, then we need to create another array to hold all our swordsmen. We would also need to modify the restore ( ) function in our window class to cycle through this new array. Imagine the number of arrays required later when we introduce ten new characters in the game!

Ah, the first problem is simple to solve. We need to use inheritance. An archer is a person and a villager is also a person. And every person has an attribute ‘life’; fairly simple.

So we convert our initial design into the following:

But what about our second problem?

One option might seem to be something on the following line of thought:

Declare a dummy draw( ) function in the class ‘person’; use a single array called ‘parr’ to store villager and archer object addresses and invoke the draw( ) function.

//the base class person

class person
{
protected:
int life;
};

//The window class member functions restore( ) and add( ) would be:

void restore( )
{
Perform till end of person array
    {
        parr[i]->draw( );
    }
}

void add_person(person *p)
{
    add ‘p’ to parr
}

//In the main( ) function we would add archers and villagers to parr

window gaming_window;
archer a1;
gaming_window.add_person(&a1);
villager v1;
gaming_window.add_person(&v1);
gaming_window.restore( );

The statement:

gaming_window.add_person(&a1);

is correct. The add_person( ) function requires address of a person but we are passing the address of an archer. Since an archer is a type of person (relationship established through inheritance), the function call is correct.

Try compiling the code and you would get a compile time error flagging the statement:

    parr[i]->draw( );

The problem is that parr is an array supposed to hold the address of person objects. A villager is a person and so ‘parr’ can hold this type of an object. But the class person doesn’t have a member function draw( ). Obviously a base class cannot be expected to know the functions present in the derived classes and so the compiler complains saying “person class does not have a member function draw( )”.

To correct this you might modify the person class as:

class person
{
protected:
    int life;

public:
    void draw( )
    {cout<<"a person";}
};

Now you won’t get a compile error and the code will run. But no matter what type of character you create, the call:

parr[i]->draw( );
will always print: Drawing a person.

Villagers and archers will never appear! Why? This is because we never specified that the code should examine the type of object and then call the appropriate draw( ) function. As far as our program is concerned, it sees parr as an array holding address of persons and thus when we try to call a member function through this address it will only call the member function present in class person (and not the ones present in the derived class).

So now we need to figure out a way to examine what sort of a object we’re holding in ‘parr’ and then call the correct darw( ) function. To examine the type of object, we need to store some information in the class which will indicate the object type. An extra member data would serve the purpose well. Our person class could be defined as:

class person
{
protected:
    int life;

public:
    int obj_type;
    void draw( )
    {cout<<"a person";}
};

The member data ‘obj_type’ will hold different values depending on whether the object is an archer or a villager. For example we might decide that an obj_type of 1 denotes a villager and value of 2 denotes an archer. The class villager would now be:

class villager:public person
{
  public:
    villager(int x=100)
    {
        obj_type=1;
        life=x;
    }
    void draw( )
    {cout<<"\nDrawing villager:"<<life;}
};

Every villager created would have obj_type as 1. Similarly in our archer class we would code: obj_type=2 in the constructor. Thus if at any point of time we want to check whether an object is of type archer or villager, we will simply check the value of obj_type.

Our modified restore( ) function in the window class would be as below. Now we’ve resolved the problem of determining what type of object address we have stored in our ‘parr’ array.

void restore( )
{
    Fetch object addresses stored in ‘parr’ array until end of array is reached
    {
        switch(parr[i]->obj_type) //check if it’s a villager or archer
        {
        case 1:
            ((villager*)parr[i])->draw( );
            break;

        case 2:
            ((archer*)parr[i])->draw( );
            break;
        }
    }
}

void add_person(person *p)
{
add ‘p’ to parr
}

To ensure that the right version of draw( ) is called we perform casting (i.e. forcing the compiler to treat a pointer to a person as a pointer to a villager/archer). If obj_type is 1 then we force the pointer parr[i] to act like a pointer to a villager. Thus when we now call draw( ) using this pointer, we are actually telling the compiler to call the draw( ) member function of the class ‘villager’.

But the drawbacks of this method are quite obvious. Casting itself is not a practice encouraged in programming (because by casting we are attempting to modify the type of an object temporarily and are skipping the type-checking operation performed by the compiler). It also clutters the code with lots of parentheses making the code appear complicated as well. And the biggest problem occurs when we want to add new characters to our game. If a swordsman has to be introduced then:

  • all objects of type swordsman should have a particular obj_type (maybe value 3).

  • We should tamper with the restore function in the window class and we will introduce more casting in the restore( ) function.

  • If there are other places where we examine obj_type then we should ensure that we modify the code in all these places to handle obj_type of 3.

  • Obviously we need some cleaner method of achieving this functionality. You might have noticed something interesting, “We are expecting our objects to respond differently to the same message!” The message we send is draw( ) and the drawing of villagers and archers are different.

    Our problems would be solved by run-time polymorphism using virtual functions. By using the keyword virtual the compiler puts that extra bit of code so that the type of the object is examined at run-time and the correct implementation is of draw( ) is called. We needn’t worry about distinguishing between different types of objects and we needn’t use an extra member data like ‘obj_type’. We also get to avoid all the casting complications because now the compiler will insert its own code so that the type of the object is determined at run-time and the correct version of the draw( ) function is called. Now we also needn’t worry about new characters being added into the game. Even if we were to introduce our swordsman, he would be derived from person (a swordsman is also a person) and thus the code of the window class needn’t be tampered with. Our code in restore( ) function will be:

    parr[i]->draw( ); 


    Go back to the Contents Page 2


    Copyright © 2005 Sethu Subramanian All rights reserved.