SrcForge

Lesson 31

Polymorphism

Covers compile-time and runtime polymorphism, virtual functions, function overriding, base class pointers, and virtual destructors.

Lesson content

What is polymorphism?

Polymorphism is the ability of a function or object to behave differently in different situations.

For example, consider the function draw(). A Circle draws a circle, a Rectangle draws a rectangle, and a Triangle draws a triangle. All objects use the same function name (draw()), but each performs a different task.

Why does polymorphism exist?

Without polymorphism, separate function names would be required for similar operations, programs would contain repetitive code, and extending applications would be more difficult.

  • Improving code reuse
  • Increasing flexibility
  • Simplifying program design
  • Making software easier to maintain

Real-world use cases

  • Game development
  • GUI frameworks
  • Graphics software
  • Banking systems
  • Plugin architectures
  • Simulation software
  • Device driver interfaces

Prerequisites

  • Classes and objects
  • Constructors and destructors
  • Inheritance
  • Function overloading
  • Pointers and references

Types of polymorphism

C++ supports two main types of polymorphism: compile-time polymorphism (also known as static polymorphism, resolved during compilation) and runtime polymorphism (also known as dynamic polymorphism, resolved during execution).

Compile-time polymorphism

Compile-time polymorphism occurs when the compiler determines which function to call before the program runs. It is achieved using function overloading, operator overloading, and function templates.

Function overloading

int add(int a, int b);

double add(double a, double b);

The compiler selects the correct function based on the argument types.

Runtime polymorphism

Runtime polymorphism is achieved using inheritance, virtual functions, function overriding, and base class pointers or references. The decision about which function to call is made during program execution.

Virtual functions

A virtual function is declared using the virtual keyword in the base class.

class Animal
{
public:
    virtual void speak()
    {
        cout << "Animal sound";
    }
};

Derived classes override the function.

Function overriding

class Dog : public Animal
{
public:
    void speak() override
    {
        cout << "Bark";
    }
};

The override keyword is optional but recommended because it helps the compiler detect mistakes.

Base class pointer

Animal* animal = new Dog();

animal->speak();

// Output: Bark

Without virtual, the base class version would be called instead.

Virtual destructor

If a base class is intended to be used polymorphically, its destructor should usually be declared virtual.

class Animal
{
public:
    virtual ~Animal()
    {
    }
};

This ensures that deleting a derived object through a base pointer calls both the derived and base destructors correctly.

Example 1: Compile-time polymorphism (function overloading)

#include <iostream>
using namespace std;

int add(int a, int b)
{
    return a + b;
}

double add(double a, double b)
{
    return a + b;
}

int main()
{
    cout << add(10, 20) << endl;

    cout << add(2.5, 4.5) << endl;

    return 0;
}

// Output:
// 30
// 7

Example 2: Runtime polymorphism

#include <iostream>
using namespace std;

class Animal
{
public:

    virtual void speak()
    {
        cout << "Animal sound" << endl;
    }
};

class Dog : public Animal
{
public:

    void speak() override
    {
        cout << "Dog barks" << endl;
    }
};

int main()
{
    Animal* animal = new Dog();

    animal->speak();

    delete animal;

    return 0;
}

// Output: Dog barks

Example 3: Multiple derived classes

#include <iostream>
using namespace std;

class Shape
{
public:

    virtual void draw()
    {
        cout << "Drawing shape" << endl;
    }
};

class Circle : public Shape
{
public:

    void draw() override
    {
        cout << "Drawing circle" << endl;
    }
};

class Rectangle : public Shape
{
public:

    void draw() override
    {
        cout << "Drawing rectangle" << endl;
    }
};

int main()
{
    Shape* shape1 = new Circle();

    Shape* shape2 = new Rectangle();

    shape1->draw();

    shape2->draw();

    delete shape1;

    delete shape2;

    return 0;
}

Example 4: Virtual destructor

#include <iostream>
using namespace std;

class Base
{
public:

    virtual ~Base()
    {
        cout << "Base Destructor" << endl;
    }
};

class Derived : public Base
{
public:

    ~Derived() override
    {
        cout << "Derived Destructor" << endl;
    }
};

int main()
{
    Base* object = new Derived();

    delete object;

    return 0;
}

// Output:
// Derived Destructor
// Base Destructor

Example 5: Polymorphism with references

#include <iostream>
using namespace std;

class Animal
{
public:

    virtual void speak()
    {
        cout << "Animal sound" << endl;
    }
};

class Cat : public Animal
{
public:

    void speak() override
    {
        cout << "Cat meows" << endl;
    }
};

void makeSound(Animal& animal)
{
    animal.speak();
}

int main()
{
    Cat cat;

    makeSound(cat);

    return 0;
}

// Output: Cat meows

Common mistakes and pitfalls

  • Forgetting the virtual keyword in the base class. Fix: declare the function as virtual
  • Forgetting to use override in the derived class. Fix: use override to catch signature mismatches
  • Deleting a derived object through a base pointer without a virtual destructor. Fix: make the base destructor virtual
  • Expecting function overloading to work at runtime. Fix: understand that overloading is compile-time polymorphism
  • Using object slicing (Base b = Derived();). Fix: use base class pointers or references

Best practices

  • Use override for every overridden virtual function
  • Declare base class destructors as virtual when using polymorphism
  • Prefer references or smart pointers over raw pointers for polymorphic code
  • Design clear interfaces in base classes
  • Keep virtual functions focused and meaningful
  • Avoid unnecessary virtual functions when polymorphism is not required

When not to use this

  • A simple function call is sufficient
  • There is no "is-a" relationship between classes
  • Compile-time techniques (such as templates) provide a simpler solution
  • Performance is critical and the small overhead of virtual dispatch is unnecessary

Summary

  • Polymorphism means one interface, many implementations
  • C++ supports compile-time and runtime polymorphism
  • Compile-time polymorphism includes function overloading, operator overloading, and templates
  • Runtime polymorphism uses inheritance, virtual functions, and function overriding
  • The virtual keyword enables dynamic dispatch
  • The override keyword improves safety and readability
  • Use a virtual destructor in polymorphic base classes
  • Prefer base class pointers or references when using runtime polymorphism

Frequently asked questions

What is polymorphism in C++?

Polymorphism is an Object-Oriented Programming feature that allows the same interface (such as a function) to behave differently depending on the object or data involved.

What is the difference between compile-time and runtime polymorphism?

Compile-time polymorphism is resolved during compilation using overloading, operator overloading, and templates, and is faster because the call is determined early. Runtime polymorphism is resolved during execution using virtual functions and overriding, and is slightly slower due to dynamic dispatch.

Why do we use the virtual keyword?

The virtual keyword tells the compiler to use dynamic dispatch, allowing the correct overridden function to be called based on the object's actual type at runtime.

Why should I use the override keyword?

override helps the compiler verify that a function truly overrides a virtual function from the base class. If the signatures don't match, the compiler reports an error.

Why is a virtual destructor important?

A virtual destructor ensures that when a derived object is deleted through a base class pointer, both the derived and base class destructors execute correctly. This prevents incomplete cleanup and potential resource leaks.