Lei Mao bio photo

Lei Mao

Machine Learning, Artificial Intelligence, Computer Science.

Twitter Facebook LinkedIn GitHub   G. Scholar E-Mail RSS

Introduction

Although there are many examples of C++ polymorphisms examples available on the Internet, it has been confusing to beginners that what exactly the polymorphism is, what kind of polymorphisms do we have in C++, and why do we really need polymorphisms.


In this blog post, I am going to elucidate these by walking through a couple of examples.

Definition

The generic definition of the word polymorphism is the occurrence of something in several different forms. In C++, it means that when calling a call to a function will cause a different function to be executed depending on the type of object that invokes the function.


More specifically, a function has a function name and inputs. When we call the functions using the same function name, supplying inputs of different types would cause different behaviors.

Categories

We have two categories of polymorphism in C++:

  • Compile time polymorphism (static polymorphism, overloading, early binding)
  • Runtime polymorphism (dynamic polymorphism, overriding, late binding)

The compile-time polymorphism could be further categorized as:

  • Function overloading
  • Operator overloading
  • Template overloading

The runtime polymorphism is:

  • Function overriding

I will walk through each of them and emphasize on the runtime polymorphism since it is usually the most confusing one.

Compile Time Polymorphism

Compile-time polymorphism, in my opinion, is relatively easy to understand. The reasons why we need this are obvious from the examples. Compile-time polymorphism, including function overloading and operator overloading, is not specific to object oriented programming. They are also used in generic functions and operators.

Examples

Function overloading and operator overloading

#include <iostream>

class Complex 
{  
public: 
    Complex(int r = 0, int i = 0)
    {
        this->real = r;
        this->img = i;
    } 
    // Operator overloading
    Complex operator + (Complex const &obj) { 
         Complex res; 
         res.real = real + obj.real; 
         res.img = img + obj.img; 
         return res; 
    } 
    void print()
    {
        std::cout << this->real << " + i" << this->img << std::endl;
    } 
private: 
    int real, img;
}; 

class Calculator
{
public:
    // Function overloading
    int add(int x, int y)
    {
        std::cout << "Add Integers!" << std::endl;
        return x + y;
    }
    float add(float x, float y)
    {
        std::cout << "Add Floats!" << std::endl;
        return x + y;
    }
    Complex add(Complex x, Complex y)
    {
        std::cout << "Add Complexes!" << std::endl;
        return x + y;
    }
};

int main()
{
    int x1{1}, y1{2};
    float x2{1.0}, y2{1.0};
    Complex x3{1,2}, y3{3,4};

    Calculator calculator;

    calculator.add(x1, y1);
    calculator.add(x2, y2);
    calculator.add(x3, y3);

    return 0;
}

Template overloading

#include <iostream>

class Complex 
{  
public: 
    Complex(int r = 0, int i = 0)
    {
        this->real = r;
        this->img = i;
    } 
    // Operator overloading
    Complex operator + (Complex const &obj) { 
         Complex res; 
         res.real = real + obj.real; 
         res.img = img + obj.img; 
         return res; 
    } 
    void print()
    {
        std::cout << this->real << " + i" << this->img << std::endl;
    } 
private: 
    int real, img;
}; 

class Calculator
{
public:
    // Template
    template <typename T>
    T add(T x, T y)
    {
        return x + y;
    }
};

int main()
{
    int x1{1}, y1{2};
    float x2{1.0}, y2{1.0};
    Complex x3{1,2}, y3{3,4};

    Calculator calculator;

    calculator.add(x1, y1);
    calculator.add(x2, y2);
    calculator.add(x3, y3);

    return 0;
}

Function Overloading

From the above example, we could see that in order to execute add for different types of data, we have implemented Calculator::add method for different input data types. At compile time, given the input types, the compiler would link the method to the corresponding methods.

Operator Overloading

Operator overloading is similar to function overloading. Function overloading is targeting function polymorphism whereas operator overloading is targeting operator polymorphism.

Template Overloading

Template overloading is simply providing an convenient way to partially save the work that we manually have to do for function overloading.

Runtime Polymorphism

As mentioned before, runtime polymorphism is just function overriding. It is achieved via virtual methods.

Examples

There are a couple of examples on the Internet which I think is misleading. One of them is like this:

#include <iostream> 

class Shape
{
public:
    Shape(std::string name) : mName(name)
    {
    }
    // Pure virtual function
    virtual int getArea() = 0;
protected:
    std::string mName;
};

class Rectangle : public Shape
{
public:
    Rectangle(int width, int height) : Shape("rectangle"), mWidth(width), mHeight(height)
    {
    }
    int getArea() override
    {
        std::cout << "The area for " << this->mName << ":" << std::endl;
        return this->mWidth * this->mHeight;
    }
private:
    int mWidth;
    int mHeight;
};

class Triangle : public Shape
{
public:
    Triangle(int base, int height) : Shape("triangle"), mBase(base), mHeight(height)
    {
    }
    int getArea() override
    {
        std::cout << "The area for " << this->mName << ":" << std::endl;
        return this->mBase * this->mHeight / 2;
    }
private:
    int mBase;
    int mHeight;
};

int main()
{
    Shape* shape;
    Rectangle rectangle(2,5);
    Triangle triangle(2,5);

    // Runtime binding
    shape = &rectangle;
    std::cout << shape->getArea() << std::endl;

    shape = &triangle;
    std::cout << shape->getArea() << std::endl;

    return 0;
}

The output of the program is:

The area for rectangle:
10
The area for triangle:
5

This tells us that a base pointer could dynamically point to the overridden virtual functions of the corresponding inherited classes. With each same call shape->getArea(), it executes different functions when shape is pointing to different inherited classes.

This polymorphism looks OK, but unlike the compile-time polymorphism, it is not obvious from this example why we really need this inheritance-based polymorphism is achieved via virtual methods. The following example, which has not used virtual methods and has almost the same amount of code as the previous one, could do the exact the same thing. Why would we need this complicated virtual methods?

#include <iostream> 

class Shape
{
public:
    Shape(std::string name) : mName(name)
    {
    }
    int getArea()
    {
        return 0;
    }
protected:
    std::string mName;
};

class Rectangle : public Shape
{
public:
    Rectangle(int width, int height) : Shape("rectangle"), mWidth(width), mHeight(height)
    {
    }
    int getArea()
    {
        std::cout << "The area for " << this->mName << ":" << std::endl;
        return this->mWidth * this->mHeight;
    }
private:
    int mWidth;
    int mHeight;
};

class Triangle : public Shape
{
public:
    Triangle(int base, int height) : Shape("triangle"), mBase(base), mHeight(height)
    {
    }
    int getArea()
    {
        std::cout << "The area for " << this->mName << ":" << std::endl;
        return this->mBase * this->mHeight / 2;
    }
private:
    int mBase;
    int mHeight;
};

int main()
{
    Shape* shape;
    Rectangle rectangle(2,5);
    Triangle triangle(2,5);

    // Early binding for non-virtual functions at runtime
    // This will not work as expected
    /*
    shape = &rectangle;
    std::cout << shape->getArea() << std::endl;
    shape = &triangle;
    std::cout << shape->getArea() << std::endl;
    */

    Rectangle* rectanglePtr = &rectangle;
    std::cout << rectanglePtr->getArea() << std::endl;

    Triangle* trianglePtr = &triangle;
    std::cout << trianglePtr->getArea() << std::endl;

    return 0;
}

I modified the example a little bit and we would see the advantages of using virtual methods for polymorphism.

#include <iostream> 
#include <vector>

class Shape
{
public:
    Shape(std::string name) : mName(name)
    {
    }
    // Pure virtual function
    virtual int getArea() = 0;
protected:
    std::string mName;
};

class Rectangle : public Shape
{
public:
    Rectangle(int width, int height) : Shape("rectangle"), mWidth(width), mHeight(height)
    {
    }
    int getArea() override
    {
        std::cout << "The area for " << this->mName << ":" << std::endl;
        return this->mWidth * this->mHeight;
    }
private:
    int mWidth;
    int mHeight;
};

class Triangle : public Shape
{
public:
    Triangle(int base, int height) : Shape("triangle"), mBase(base), mHeight(height)
    {
    }
    int getArea() override
    {
        std::cout << "The area for " << this->mName << ":" << std::endl;
        return this->mBase * this->mHeight / 2;
    }
private:
    int mBase;
    int mHeight;
};

int computeArea(Shape* shape)
{
    /* We may also have 1000 lines code doing boring stuff here */
    std::cout << shape->getArea() << std::endl;
}

int main()
{
    Rectangle rectangle(2,5);
    Triangle triangle(2,5);

    computeArea(&rectangle);
    computeArea(&triangle);

    return 0;
}

Function Overriding

Here I used computeArea to compute the area of a given shape, and we pretend this computeArea function is very long and the variable shape would be used a lot of times in the function. By just implementing the compute computeArea once, we would be able to compute the area for any Shape whose getArea method has been implemented. We don’t have to use function overloading to implement the (potentially very long) computeArea for Triangle, Rectangle, or any other new shapes.

Compile Time Polymorphism VS Runtime Polymorphism

Virtual VS Template

OK, someone may still argue that using compile-time polymorphism template, we can do the same thing without implementing computeArea for different Shapes using the following implementation.

#include <iostream> 

class Shape
{
public:
    Shape(std::string name) : mName(name)
    {
    }
    int getArea()
    {
        return 0;
    }
protected:
    std::string mName;
};

class Rectangle : public Shape
{
public:
    Rectangle(int width, int height) : Shape("rectangle"), mWidth(width), mHeight(height)
    {
    }
    int getArea()
    {
        std::cout << "The area for " << this->mName << ":" << std::endl;
        return this->mWidth * this->mHeight;
    }
private:
    int mWidth;
    int mHeight;
};

class Triangle : public Shape
{
public:
    Triangle(int base, int height) : Shape("triangle"), mBase(base), mHeight(height)
    {
    }
    int getArea()
    {
        std::cout << "The area for " << this->mName << ":" << std::endl;
        return this->mBase * this->mHeight / 2;
    }
private:
    int mBase;
    int mHeight;
};

template <typename T>
int computeArea(T* shape)
{
    /* We may also have 1000 lines code doing boring stuff here */
    std::cout << shape->getArea() << std::endl;
}

int main()
{
    Shape* shape;
    Rectangle rectangle(2,5);
    Triangle triangle(2,5);

    computeArea(&rectangle);
    computeArea(&triangle);

    return 0;
}

So it looks like we could do the exact same thing using either virtual methods or template. Using template has better performance since it is compile-time polymorphism. However, which one to use in practice is a little bit ambiguous to me. Probably using virtual methods is more “object-oriented” and it does save the memory consumption of the program if there are many classes inheritances.

Miscellaneous

VTable

Here is an interesting question. How does the base pointer know which is the correct virtual method to call?


If a class contains a virtual function then compiler itself does two things:

  • Irrespective of an instance of the class is created or not, a static array of function pointer called VTable, where each cell contains the address of each virtual function contained in that class, will be created.
  • Each of the instances of the class created will have a virtual pointer (VPtr) as a member variable pointing to the VTable of the class.

So when we have a base pointer pointing to the instance of the inherited class, we could get access to the VPtr of the instance. Depending on the virtual method we called, the corresponding function on the VTable would be found via VPtr and executed.