Polymorphism

Consider the Shape class, and its descendents, defined in previous lectures. A Shape is a kind of “abstract” category that cannot be used to perform any real task. For example, you cannot draw a shape, or find its area. At least, that’s how we are thinking about shapes. When we defined the Shape class, we did not provide any of these methods (e.g. draw(), area(), etc.). However, subclasses like Rectangle and Ellipse did have some useful methods (e.g. area()). That’s because, obviously, one can actually draw a rectangle or calculate its area.

We already know that using object-orientation and inheritance in particular is a good idea for shapes; that’s because rectangles, ellipses, and so on share many common properties and methods, such as the properties x and y (location in a 2D space), and perhaps some methods like translate(), rotate(), and so on. However, we were unable to put the area() method in the Shape class, even though all subclasses will have their own area() methods, because there is no concept of the “area” of a “generic shape.” So instead we just defined the area() method for each subclass.

Now, there is a significant problem with this state of affairs we have constructed. We’re not getting the full power of object-orientation and inheritance. We all know that every shape has an area, so any object that is a Shape, by way of inheritance, should have an area() method. Recall that if we create a Rectangle object, say Rectangle r(3, 5); then we are simultaneously creating a Shape object as well; because Rectangle is a subclass of Shape, any Rectangle object is also a Shape object. That means you can change a Rectangle object into a Shape object:

// this is valid since Rectangle inherits from Shape
Shape s = Rectangle(3, 5);

// or using pointers instead:
Shape *s = new Rectangle(3, 5);

However, even though s really is a Rectangle, now that we are treating it as a Shape we cannot ask for its area, because the Shape class does not have an area() method.

However, we often find it convenient, or even necessary, to gather up several objects of several distinct types (different classes) which all happen to inherit from the same parent class. In our shapes example, we might want to gather up several objects of several kinds of shapes, and put them into a vector:

// make a few shapes
Triangle t(3, 4, 3.141/2.0);
Rectangle r(8, 12);
Ellipse e(3.4, 3.4);

vector<Shape> myshapes;
myshapes.push_back(t);
myshapes.push_back(r);
myshapes.push_back(e);

// or using pointers
vector<Shape*> myshapes2;
myshapes2.push_back(&t);
myshapes2.push_back(&r);
myshapes2.push_back(&e);

However, now that they are all considered just shapes and not rectangles, ellipses, and so on, the objects in the vector can only be treated as objects of type Shape. So, in particular, we can’t ask any object in the vector to calculate its area.

This is the problem that polymorphism solves. Polymorphism means that an object of some class, say r of the class Rectangle, can “look like” a Shape object but act like a Rectangle object when its asked to do things that may be done differently by a Rectangle.

Consider a different example: human infants are humans, and humans walk on two feet. Perhaps you have defined a class named Human and a class named HumanInfant (which is a subclass of Human). The Human class may have a method move() that visually animates a human walking on two feet. The HumanInfant will also have a method called move() but what you’ll see is a baby crawling. So of course the code for the two move() methods will be different. Now consider that you have a collection of Human objects, say a vector called characters_in_the_game, and you want to tell each character to move in some random direction. You want the correct move() method to work on each character. In this example, you will have Human objects and HumanInfant objects, but they will all “look like” Human objects when the are put into the vector (because vectors can only hold one type of thing, and you don’t want to change Human objects into HumanInfant objects; rather, the other way around is more appropriate, because the class hierarchy says HumanInfants are Humans, not vice versa). Even though the baby characters will appear to be regular adult characters (because every object in the vector will be of the type Human-pointer, i.e. Human*), when the move() method is called for each object in the array, the HumanInfant’s move() method is called if the object is actually of that type, and not of the type Human. This is polymorphism (an object looking like another, as far as types are concerned, but behaving in its specific way).

To achieve this effect with our Shape class, we indicate that the area() method is virtual. By virtual we mean polymorphic (but the C++ creator thought virtual was a better word, it seems). Here is the modified class:

class Shape
{
public:
    double x;
    double y;

    // this method is virtual so that subclasses can redefine it
    virtual double area()
    {
        // if this method is used with a true generic shape object,
        // just return 0.0
        return 0.0;
    }
};

The subclasses need not change at all.

Finally, this Shape class should not really have an area() method at all, since we don’t ever want to create true generic Shape objects. Rather, we will only be creating Rectangle, Ellipse, etc. objects. So we delete the code for the area() method in the Shape class and just write = 0; instead to indicate that subclasses will have this area() method but the Shape class will not. By doing this, we are turning area() into a “pure virtual” method, and thus changing the Shape class into an “abstract class.” The reason for this extra terminology is that we will no longer be able to create Shape objects (e.g. Shape s; which we never attempted to do anyway) because the Shape class has a method with no definition (a purely virtual method). This makes the Shape class a kind of “contract” which says “any subclasses must provide code for their own area() method because I am introducing such a method but not providing any code for it.”

class Shape
{
public:
    double x;
    double y;

    // this method is purely virtual; subclasses *must* define it
    virtual double area() = 0;
};

Now that Shape is an abstract class, we can’t turn Rectangle or whatever into Shape; we can only use the Shape class as a pointer to a true Rectangle, Ellipse, etc.

// not possible; ERROR!
Shape s = Rectangle(3, 4);

// this is ok
Shape *s = new Rectangle(3, 4);

And we can ask for the rectangle’s area even though it “looks like” a Shape:

// polymorphism is used here
Shape *s = new Rectangle(3, 4);
cout << s->area() << endl;

Here is another, completely different example.

// stolen from http://en.wikipedia.org/wiki/Virtual_function

#include <iostream>
#include <vector>
using namespace std;
 
class Animal
{
public:
    virtual void eat()
    { 
        cout << "I eat like a generic Animal." << endl; 
    }
};
 
class Wolf : public Animal
{
public:
    void eat()
    { 
        cout << "I eat like a wolf!" << endl; 
    }
};
 
class Fish : public Animal
{
public:
    void eat()
    {  
        cout << "I eat like a fish!" << endl; 
    }
};
 
class GoldFish : public Fish
{
public:
    void eat()
    { 
        cout << "I eat like a goldfish!" << endl; 
    }
};
 
class OtherAnimal : public Animal
{
    // does nothing special
};
 
int main()
{
    vector<Animal*> animals;
    animals.push_back(new Animal());
    animals.push_back(new Wolf());
    animals.push_back(new Fish());
    animals.push_back(new GoldFish());
    animals.push_back(new OtherAnimal());
 
    for(int i = 0; i < animals.size(); i++)
    {
        animals[i]->eat();
    }
 
    return 0;
}

Output:

I eat like a generic Animal.
I eat like a wolf!
I eat like a fish!
I eat like a goldfish!
I eat like a generic Animal.

If we did not use Animal pointers, but instead put instances of each class into the vector (rather than pointers), like so:

vector<Animal> animals;
animals.push_back(Animal());
animals.push_back(Wolf());
animals.push_back(Fish());
animals.push_back(GoldFish());
animals.push_back(OtherAnimal());

for(int i = 0; i < animals.size(); i++)
{
    animals[i].eat();
}

…then polymorphism would not take effect. This would be the output:

I eat like a generic Animal.
I eat like a generic Animal.
I eat like a generic Animal.
I eat like a generic Animal.
I eat like a generic Animal.
CSE 230 material by Joshua Eckroth is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License. Source code for this website available at GitHub.