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.