Virtual Functions and Polymorphism

Programming Workshop 2 (CSCI 1061U)

Faisal Qureshi

Faculty of Science, UOIT

http://vclab.science.uoit.ca


Virtual functions and Polymorphism

Polymorphism is a key principle of object-oriented programming. It enables us to assocaite many meanings to a single function. Virtual functions provide this capability.

Consider the following shapes.h and shapes.cpp files that implement three classes: Shape, Circle, and Rectangle. Classes Circle and Rectangle are inherited from class Shape.

shapes.h

class Shape {
public:
    Shape();
    ~Shape();
    void draw(); // draws an empty shape
};

class Rectangle : public Shape
{
public:
    Rectangle(int w, int h);
    ~Rectangle();
    void draw(); // draws a rectangle

private:
    int _w, _h; // width & height
};

class Circle : public Shape
{
public:
    Circle(int r);
    ~Circle();
    void draw(); // draws a circle

private:
    int _r; // radius
};

shapes.cpp

#include "Shapes.h"
#include <iostream>
using namespace std;

Shape::Shape() {}

Shape::~Shape() {}

void Shape::draw()
{
    cout << "Drawing an empty shape" << endl;
}

/////////////////////////////////////////////
Rectangle::Rectangle(int w, int h)
    : _w(w), _h(h)   
{}

Rectangle::~Rectangle() {}

void Rectangle::draw()
{
    cout << "Drawing a rectangle of width " << _w 
         << " and height " << _h 
         << endl;    
}

/////////////////////////////////////////////
Circle::Circle(int r)
    : _r(r) 
{}

Circle::~Circle() {}

void Circle::draw()
{
    cout << "Drawing a circle of radius " << _r << endl;
}

Now lets consider the following main.cpp file.

main.cpp

#include "Shapes.h"

int main()
{
    Circle* c = new Circle(2);
    c->draw(); // this draws circle

    Rectangle* r = new Rectangle(3,6);
    r->draw(); // this draws rectangle

    delete c; delete r; 
}

If we compile and run this program we get

> g++ main.cpp shapes.cpp -o ex1
> ./ex1
Drawing a circle of radius 2
Drawing a rectangle of width 3 and height 6

This is as we expected.

Now lets consider another situation. Consider the following program that stores a list of shapes in an array.

main2.cpp

#include "Shapes.h"

int main()
{
    Shape* s[2]; // a list of different kind of shapes
    s[0] = new Circle(4);
    s[1] = new Rectangle(4,5);

    for (int i=0; i<2; ++i) {
        s[i]->draw();
    }

    for (int i=0; i<2; ++i) {
        delete s[i];
    }
}

We know that an array only store elements of the same type. The array s in the above code stores pointers to Shape. We assign a pointer to a Circle instance to s[0] and a pointer to a Rectangle instance to s[1]. This is allowed since both Circle and Rectangle are subclasses of Shape. Recall that Circle is a Shape, and Rectangle is a Shape.

However, when we compile and run the above program, we get the following

> g++ main2.cpp shapes.cpp -o ex2
> ./ex2
Drawing an empty shape
Drawing an empty shape

It seems that the s[i]->draw() isn't behaving as we expected. Obviously, the draw function in class Shape is being called. We want the draw functions of the respective classes to be called. s[0]->draw() should call the draw function in Circle class. Similarly s[1]->draw() should call the draw function in Rectangle class. How do we do it?

We can use late binding to discern the actual type of the pointer and then call the appropriate function. So, we can discern that s[0] is actually a pointer to a Circle instance. In C++, we achieve this be declaring a function virtual. Specifically, we will modify the parent class as follows

class Shape {
public:
    Shape();
    ~Shape();
    virtual void draw(); // draws an empty shape
};

By declaring the draw function virtual in the parent class, we assert that function implementation is not known at compile time. Furthermore, that function implementation should be inferred from object instance at runtime. Note that in C++ late binding is performed only for functions declared virtual.

If we compile and execute the program now, we get the correct behavior

> g++ main2.cpp shapes.cpp -o ex2
> ./ex2
Drawing a circle of radius 4
Drawing a rectangle of width 4 and height 5

Overriding

If virtual function definition is changed in the derived class, we say that it has been overridden. When a non-virtual function is changed in the derived class, we say that the function has been redefined.

C++11 includes override keyword that makes it clear whether a function overrides a function in the parent class.

class Sale:
{
    public:
    virtual double bill() const;
};

class DiscountSale: public Sale
{
    public:
    double bill() const override;
};

C++11 final keyword

C++11 includes the final keyword to prevent a function from being overridden. Useful if a function is overridden but don’t want a derived classes to override it again.

class Sale:
{
    public:
    virtual double bill() const final;
};

class DiscountSale: public Sale
{
    public:
    double bill() const; // This will give a compiler error
};

Pure virtual function

Base classes may not have a meaningful definitions for some of its functions. The purpose of the base class is solely for others to derive from it. It is possible to create a pure virtual function as follows

class Shape
{
    public:
    virtual draw() = 0; // Pure virtual function
};

This has two consquences. First, it is not possible to create an instance of Shape class. Shape class is now an abstract class. We cannot create an instance of Shape class, since it is missing definition of one of its functions. Second, it a derived (from Shape) class must implement draw. If the derived class doesn't implmenet draw the the derived class is also an abstract class.

Overhead

Virtual functions play an important role; however, they do come with an overhead. These use more storage. The programs also run slower due to late binding, which is performed on the fly. Only use these when needed.

When in doubt make destructors virtual. This would prevent memory leaks.

References