Skip to content

C++ Polymorphism

The term polymorphism comes from Greek words meaning “many forms”. In C++, polymorphism usually refers to the ability of objects of different classes, related by inheritance, to respond differently to the same function call. This typically involves a base class pointer or reference pointing to a derived class object.

Specifically, C++ runtime polymorphism means that when you call a member function through a base class pointer or reference, the function executed is determined at runtime based on the actual type of the object being pointed to, not the static type of the pointer/reference.

Static vs. Dynamic Binding (Early vs. Late Binding)

Section titled “Static vs. Dynamic Binding (Early vs. Late Binding)”

Consider this example without polymorphism:

#include <iostream>
#include <memory> // For std::unique_ptr
// Base Class
class Shape {
protected:
int width, height;
public:
// Constructor using initializer list
Shape(int w = 0, int h = 0) : width{w}, height{h} {}
// Non-virtual function
int area() const { // Added const
std::cout << "Base Shape::area() called." << std::endl;
return 0; // Base class might not have a meaningful area
}
};
// Derived Class 1
class Rectangle : public Shape {
public:
Rectangle(int w = 0, int h = 0) : Shape{w, h} {}
int area() const { // Hides the base class function, doesn't override
std::cout << "Rectangle::area() called." << std::endl;
return (width * height);
}
};
// Derived Class 2
class Triangle : public Shape {
public:
Triangle(int w = 0, int h = 0) : Shape{w, h} {}
int area() const { // Hides the base class function, doesn't override
std::cout << "Triangle::area() called." << std::endl;
return (width * height / 2);
}
};
int main() {
Rectangle rect(10, 7);
Triangle tri(10, 5);
Shape* shapePtr; // Base class pointer
// Point to Rectangle
shapePtr = &rect;
std::cout << "Calling area() via base pointer (pointing to Rectangle): ";
shapePtr->area(); // Calls Shape::area()
// Point to Triangle
shapePtr = &tri;
std::cout << "Calling area() via base pointer (pointing to Triangle): ";
shapePtr->area(); // Calls Shape::area()
return 0;
}

When compiled and executed, this produces:

Calling area() via base pointer (pointing to Rectangle): Base Shape::area() called.
Calling area() via base pointer (pointing to Triangle): Base Shape::area() called.

Why the incorrect output? Because the area() function is non-virtual. The compiler resolves the function call based on the static type of the pointer (Shape*), not the actual type of the object it points to at runtime. This is called static binding or early binding.

Achieving Polymorphism with virtual Functions

Section titled “Achieving Polymorphism with virtual Functions”

To enable polymorphism, we declare the base class function with the virtual keyword. This tells the compiler to use dynamic binding (or late binding), resolving the function call at runtime based on the object’s actual type.

Let’s modify the Shape class:

// Base Class with virtual function
class Shape {
protected:
int width, height;
public:
Shape(int w = 0, int h = 0) : width{w}, height{h} {}
// Declare area() as virtual
virtual int area() const {
std::cout << "Base Shape::area() called." << std::endl;
return 0;
}
// Virtual destructor (Important!)
virtual ~Shape() {
std::cout << "Base Shape destructor called." << std::endl;
}
};

It’s also good practice to add the override keyword in derived classes to ensure you are actually overriding a base class virtual function:

// Derived Class 1 (Rectangle)
class Rectangle : public Shape {
public:
Rectangle(int w = 0, int h = 0) : Shape{w, h} {}
// Use 'override' for clarity and safety
int area() const override {
std::cout << "Rectangle::area() called." << std::endl;
return (width * height);
}
~Rectangle() override {
std::cout << "Rectangle destructor called." << std::endl;
}
};
// Derived Class 2 (Triangle)
class Triangle : public Shape {
public:
Triangle(int w = 0, int h = 0) : Shape{w, h} {}
// Use 'override'
int area() const override {
std::cout << "Triangle::area() called." << std::endl;
return (width * height / 2);
}
~Triangle() override {
std::cout << "Triangle destructor called." << std::endl;
}
};

Now, running the same main function (preferably using smart pointers like std::unique_ptr for safer memory management) produces the polymorphic behavior:

// Modified main using smart pointers and demonstrating polymorphism
int main() {
std::unique_ptr<Shape> shapePtr;
// Point to Rectangle
shapePtr = std::make_unique<Rectangle>(10, 7);
std::cout << "Calling area() via base unique_ptr (holding Rectangle): ";
int rectArea = shapePtr->area(); // Calls Rectangle::area()
std::cout << "Rectangle Area: " << rectArea << std::endl;
// Point to Triangle
shapePtr = std::make_unique<Triangle>(10, 5);
std::cout << "Calling area() via base unique_ptr (holding Triangle): ";
int triArea = shapePtr->area(); // Calls Triangle::area()
std::cout << "Triangle Area: " << triArea << std::endl;
return 0; // unique_ptr automatically manages memory release and calls appropriate destructors
}

Expected output (including destructor calls):

Calling area() via base unique_ptr (holding Rectangle): Rectangle::area() called.
Rectangle Area: 70
Rectangle destructor called.
Base Shape destructor called.
Calling area() via base unique_ptr (holding Triangle): Triangle::area() called.
Triangle Area: 25
Triangle destructor called.
Base Shape destructor called.

Key Point: Polymorphism works with pointers and references to base classes. Using virtual functions enables the program to decide at runtime which version of the function to call based on the object’s actual type.

If a class is intended to be a base class in a hierarchy, and you might delete derived objects through a base class pointer, the base class must have a virtual destructor. Otherwise, only the base class destructor will be called, potentially leading to resource leaks if derived classes manage resources. Notice the virtual ~Shape() in the updated example.

Abstract Classes and Pure Virtual Functions

Section titled “Abstract Classes and Pure Virtual Functions”

Sometimes, a base class provides an interface or a concept, but cannot provide a meaningful implementation for a virtual function. For example, what’s the ‘area’ of a generic ‘Shape’? In such cases, we can declare the function as a pure virtual function.

A pure virtual function is declared by appending = 0 to its declaration:

// Base Class as an Abstract Class
class Shape {
protected:
int width, height;
public:
Shape(int w = 0, int h = 0) : width{w}, height{h} {}
// Pure virtual function - makes Shape an abstract class
virtual int area() const = 0;
// Virtual destructor is still crucial for abstract base classes
virtual ~Shape() = default; // Use default if no custom cleanup needed
};

A class containing one or more pure virtual functions is called an abstract class. You cannot create objects directly from an abstract class (e.g., Shape myShape; would be an error). Abstract classes serve as interfaces that derived classes must implement. Any derived class that does not override and provide an implementation for all inherited pure virtual functions will also be abstract.

You can prevent a virtual function from being overridden further down the inheritance chain by marking it final in the derived class:

class Circle : public Shape {
// ...
int area() const override final {
// Implementation...
// No class derived from Circle can override area()
}
// ...
};

You can also mark an entire class as final to prevent it from being inherited:

class Uninheritable final { /* ... */ };
// class Derived : public Uninheritable {}; // Error!