Polymorphism

Polymorphism, derived from Greek meaning "many shapes", is a fundamental concept in object-oriented programming (OOP). It allows objects of different classes to be treated as objects of a common superclass. In C++, polymorphism can be achieved through function overloading, function overriding, and using virtual functions.

Types of Polymorphism in C++

  1. Compile-time Polymorphism: This is achieved through function overloading and templates.
  2. Run-time Polymorphism: This is achieved through virtual functions and inheritance.

Compile-time Polymorphism

Function Overloading

Function overloading allows you to define multiple functions with the same name but different parameters. The compiler determines which function to call based on the number and type of arguments passed.

				
					#include <iostream>

void display(int num) {
    std::cout << "Integer: " << num << std::endl;
}

void display(double num) {
    std::cout << "Double: " << num << std::endl;
}

int main() {
    display(5);       // Calls display(int)
    display(3.14);    // Calls display(double)
    return 0;
}

				
			
				
					// output //
Integer: 5
Double: 3.14


				
			

Templates

Templates in C++ provide a way to create functions and classes that work with any data type. They allow you to write generic code that can be reused with different data types without having to rewrite the code for each type.

Function Templates

Function templates allow you to define a function template once and use it with different data types. They are declared using the template keyword followed by a list of template parameters enclosed in angle brackets (<>).

				
					#include <iostream>

template<typename T>
T add(T a, T b) {
    return a + b;
}

int main() {
    std::cout << add(5, 3) << std::endl;        // Calling with integers
    std::cout << add(3.5, 2.5) << std::endl;    // Calling with doubles
    return 0;
}

				
			
				
					// output //
8
6

				
			

In this example, add() is a function template that can add two values of any type (int, double, etc.) because it’s parameterized with the template parameter T.

Note : We will discuss more about templates further in detail with individual topic 

Run-time Polymorphism

Virtual Functions and Inheritance

Virtual functions allow you to achieve run-time polymorphism by enabling dynamic method binding. They are functions declared in a base class and overridden in derived classes.

				
					#include <iostream>

class Animal {
public:
    virtual void sound() {
        std::cout << "Animal makes a sound" << std::endl;
    }
};

class Dog : public Animal {
public:
    void sound() override {
        std::cout << "Dog barks" << std::endl;
    }
};

class Cat : public Animal {
public:
    void sound() override {
        std::cout << "Cat meows" << std::endl;
    }
};

int main() {
    Animal* ptr;
    
    Dog dog;
    Cat cat;
    
    ptr = &dog;
    ptr->sound();   // Calls Dog's sound()
    
    ptr = &cat;
    ptr->sound();   // Calls Cat's sound()
    
    return 0;
}

				
			
				
					// output //
Dog barks
Cat meows

				
			

Real World Example

Consider a scenario where you have a base class Shape and derived classes Rectangle and Circle. Each derived class implements its own version of the area() function.

				
					#include <iostream>

class Shape {
public:
    virtual float area() {
        return 0;
    }
};

class Rectangle : public Shape {
private:
    float length;
    float width;
public:
    Rectangle(float l, float w) : length(l), width(w) {}
    float area() override {
        return length * width;
    }
};

class Circle : public Shape {
private:
    float radius;
public:
    Circle(float r) : radius(r) {}
    float area() override {
        return 3.14 * radius * radius;
    }
};

int main() {
    Shape* shapes[2];
    shapes[0] = new Rectangle(5, 3);
    shapes[1] = new Circle(4);

    for (int i = 0; i < 2; ++i) {
        std::cout << "Area of shape " << i+1 << ": " << shapes[i]->area() << std::endl;
        delete shapes[i];
    }

    return 0;
}

				
			
				
					// output //
Area of shape 1: 15
Area of shape 2: 50.24

				
			

Advanced Concepts

Pure Virtual Functions and Abstract Classes

Abstract classes are classes that cannot be instantiated and may contain one or more pure virtual functions. A pure virtual function is a virtual function with no implementation in the base class. It must be overridden in derived classes.

				
					#include <iostream>

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

class Rectangle : public Shape {
private:
    float length;
    float width;
public:
    Rectangle(float l, float w) : length(l), width(w) {}
    float area() override {
        return length * width;
    }
};

int main() {
    // Shape shape; // Error: Cannot instantiate abstract class
    Rectangle rectangle(5, 3);
    std::cout << "Area of rectangle: " << rectangle.area() << std::endl;
    return 0;
}

				
			
				
					// output //
Area of rectangle: 15

				
			

Explanation:

  • Shape Class:
  • It’s an abstract class with a pure virtual function area(), denoted by = 0.
  • The area() function is declared without a body, making Shape an abstract class. Abstract classes cannot be instantiated; they are meant to serve as base classes for other classes.
  • Rectangle Class:
  • It inherits from the Shape class.
  • It implements the area() function to calculate the area of a rectangle by multiplying its length and width.
  • Main Function:
  • Instantiating objects of abstract classes is not allowed (Shape shape;), so it’s commented out. This line would result in a compilation error.
  • An object of the Rectangle class is created with a length of 5 and a width of 3.
  • The area() method of the Rectangle class is called to calculate the area of the rectangle.
  • The result is printed to the console.

Virtual Destructors

When working with polymorphism and inheritance, it’s important to declare the base class destructor as virtual. This ensures that the appropriate destructor is called when deleting objects through base class pointers.

				
					#include <iostream>

class Base {
public:
    virtual ~Base() {
        std::cout << "Base destructor" << std::endl;
    }
};

class Derived : public Base {
public:
    ~Derived() override {
        std::cout << "Derived destructor" << std::endl;
    }
};

int main() {
    Base* ptr = new Derived();
    delete ptr; // Calls Derived destructor
    return 0;
}

				
			
				
					// output //
Derived destructor
Base destructor

				
			

Explanation:

  • The program first calls the destructor of the Derived class because ptr points to an object of the Derived class. This prints “Derived destructor”.
  • Then, the program calls the destructor of the Base class (the base class destructor is called automatically after the derived class destructor) because the Base class destructor is marked as virtual. This prints “Base destructor”.

Exception Handling with Polymorphism

Exception Hierarchies

Polymorphism can also be applied to exception handling. You can define an exception hierarchy with base and derived exception classes, allowing for more specific error handling.

				
					#include <iostream>
#include <exception>

class BaseException : public std::exception {
public:
    const char* what() const noexcept override {
        return "Base Exception";
    }
};

class DerivedException : public BaseException {
public:
    const char* what() const noexcept override {
        return "Derived Exception";
    }
};

int main() {
    try {
        throw DerivedException();
    } catch (BaseException& e) {
        std::cout << "Caught: " << e.what() << std::endl;
    }
    return 0;
}

				
			
				
					// output //
Caught: Derived Exception

				
			

Explanation:

  • Even though the exception thrown is of type DerivedException, it is caught by reference to BaseException in the catch block.
  • Since BaseException is the base class of DerivedException, polymorphism allows the catch block to catch the exception object.
  • When e.what() is called inside the catch block, it resolves to the overridden what() method of DerivedException, printing “Derived Exception”.

Polymorphism in C++ allows for flexibility and extensibility in code. By leveraging compile-time and run-time polymorphism, you can write more modular, maintainable, and scalable programs. Function overloading and templates enable compile-time polymorphism, while virtual functions and inheritance facilitate run-time polymorphism, making C++ a powerful language for object-oriented programming.Happy coding !❤️

Table of Contents