Polymorphism

Polymorphism is one of the four fundamental principles of object-oriented programming (OOP). The word “polymorphism” comes from Greek words meaning “many forms.” In C++, polymorphism allows objects of different classes to be treated as objects of a common base class.

What is Polymorphism?

Polymorphism allows us to perform a single action in different ways. It provides a way to use a class exactly like its parent class but with its own specific implementation of some methods.

In simpler terms, polymorphism means that a call to a member function will cause different implementations to be executed depending on the type of object that invokes the function.

Types of Polymorphism in C++

C++ supports two types of polymorphism:

  1. Compile-time Polymorphism (Static Binding or Early Binding)
  2. Runtime Polymorphism (Dynamic Binding or Late Binding)

Compile-time Polymorphism

Compile-time polymorphism is resolved during the compilation of the program. In C++, compile-time polymorphism is achieved using:

  1. Function Overloading
  2. Operator Overloading
  3. Templates

Function Overloading

Function overloading is a feature in C++ where two or more functions can have the same name but different parameters (different number, types, or order of parameters).

#include <iostream>
using namespace std;

// Function with one integer parameter
void print(int i) {
    cout << "Integer value: " << i << endl;
}

// Function with one double parameter
void print(double d) {
    cout << "Double value: " << d << endl;
}

// Function with one character parameter
void print(char c) {
    cout << "Character value: " << c << endl;
}

// Function with two integer parameters
void print(int i, int j) {
    cout << "Two integer values: " << i << " and " << j << endl;
}

int main() {
    print(10);      // Calls print(int)
    print(10.5);    // Calls print(double)
    print('a');     // Calls print(char)
    print(10, 20);  // Calls print(int, int)
    
    return 0;
}

Output:

Integer value: 10
Double value: 10.5
Character value: a
Two integer values: 10 and 20

The compiler determines which function to call based on the arguments passed. This decision is made at compile time.

Operator Overloading

Operator overloading allows operators to work with user-defined data types.

#include <iostream>
using namespace std;

class Complex {
private:
    double real;
    double imag;
    
public:
    // Constructor
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}
    
    // Overload the + operator
    Complex operator + (const Complex& obj) {
        Complex result;
        result.real = real + obj.real;
        result.imag = imag + obj.imag;
        return result;
    }
    
    // Display the complex number
    void display() {
        cout << real << " + " << imag << "i" << endl;
    }
};

int main() {
    Complex c1(3, 4);
    Complex c2(5, 6);
    
    // Use the overloaded + operator
    Complex c3 = c1 + c2;
    
    cout << "c1: ";
    c1.display();
    
    cout << "c2: ";
    c2.display();
    
    cout << "c1 + c2: ";
    c3.display();
    
    return 0;
}

Output:

c1: 3 + 4i
c2: 5 + 6i
c1 + c2: 8 + 10i

Templates

Templates allow functions and classes to operate with generic types.

#include <iostream>
using namespace std;

// Template function to swap any type of data
template <typename T>
void swap_values(T& a, T& b) {
    T temp = a;
    a = b;
    b = temp;
}

int main() {
    int a = 5, b = 10;
    cout << "Before swap: a = " << a << ", b = " << b << endl;
    swap_values(a, b);
    cout << "After swap: a = " << a << ", b = " << b << endl;
    
    double x = 3.14, y = 2.71;
    cout << "Before swap: x = " << x << ", y = " << y << endl;
    swap_values(x, y);
    cout << "After swap: x = " << x << ", y = " << y << endl;
    
    char c1 = 'A', c2 = 'B';
    cout << "Before swap: c1 = " << c1 << ", c2 = " << c2 << endl;
    swap_values(c1, c2);
    cout << "After swap: c1 = " << c1 << ", c2 = " << c2 << endl;
    
    return 0;
}

Output:

Before swap: a = 5, b = 10
After swap: a = 10, b = 5
Before swap: x = 3.14, y = 2.71
After swap: x = 2.71, y = 3.14
Before swap: c1 = A, c2 = B
After swap: c1 = B, c2 = A

Runtime Polymorphism

Runtime polymorphism is resolved during the execution of the program. In C++, runtime polymorphism is achieved using:

  1. Virtual Functions
  2. Function Overriding

Virtual Functions and Function Overriding

Virtual functions allow us to create a list of base class pointers and call methods of derived classes through them. This is the key to achieving runtime polymorphism.

#include <iostream>
using namespace std;

// Base class
class Animal {
public:
    virtual void speak() {
        cout << "Animal speaks" << endl;
    }
};

// Derived class 1
class Dog : public Animal {
public:
    void speak() override {
        cout << "Dog barks: Woof! Woof!" << endl;
    }
};

// Derived class 2
class Cat : public Animal {
public:
    void speak() override {
        cout << "Cat meows: Meow! Meow!" << endl;
    }
};

// Derived class 3
class Duck : public Animal {
public:
    void speak() override {
        cout << "Duck quacks: Quack! Quack!" << endl;
    }
};

int main() {
    // Create objects of different classes
    Animal *animals[4];
    animals[0] = new Animal();
    animals[1] = new Dog();
    animals[2] = new Cat();
    animals[3] = new Duck();
    
    // Call the speak() method for each object
    for (int i = 0; i < 4; i++) {
        animals[i]->speak();
    }
    
    // Free allocated memory
    for (int i = 0; i < 4; i++) {
        delete animals[i];
    }
    
    return 0;
}

Output:

Animal speaks
Dog barks: Woof! Woof!
Cat meows: Meow! Meow!
Duck quacks: Quack! Quack!

The decision about which function to call is made at runtime based on the type of the object being pointed to, not on the type of the pointer.

Key Differences: Compile-time vs. Runtime Polymorphism

FeatureCompile-time PolymorphismRuntime Polymorphism
Resolving timeDuring compilationDuring execution
Function bindingStatic binding (early binding)Dynamic binding (late binding)
FlexibilityLess flexibleMore flexible
PerformanceGenerally fasterSlightly slower due to vtable lookup
ImplementationFunction overloading, Operator overloading, TemplatesVirtual functions, Function overriding
Examplesoperator+(), function overloadingBase class pointer to derived class object

Real-World Example: Shape Drawing Application

Let’s look at a real-world example where runtime polymorphism is used to create a simple shape drawing application:

#include <iostream>
#include <vector>
#include <string>
using namespace std;

// Base class
class Shape {
protected:
    string color;
    
public:
    Shape(string c) : color(c) {}
    
    // Virtual function for drawing a shape
    virtual void draw() {
        cout << "Drawing a shape" << endl;
    }
    
    // Virtual function for calculating area
    virtual double area() {
        return 0.0;
    }
    
    // Virtual destructor
    virtual ~Shape() {}
};

// Derived class - Circle
class Circle : public Shape {
private:
    double radius;
    const double PI = 3.14159;
    
public:
    Circle(string c, double r) : Shape(c), radius(r) {}
    
    void draw() override {
        cout << "Drawing a " << color << " circle with radius " << radius << endl;
    }
    
    double area() override {
        return PI * radius * radius;
    }
};

// Derived class - Rectangle
class Rectangle : public Shape {
private:
    double width;
    double height;
    
public:
    Rectangle(string c, double w, double h) : Shape(c), width(w), height(h) {}
    
    void draw() override {
        cout << "Drawing a " << color << " rectangle with width " << width << " and height " << height << endl;
    }
    
    double area() override {
        return width * height;
    }
};

// Derived class - Triangle
class Triangle : public Shape {
private:
    double base;
    double height;
    
public:
    Triangle(string c, double b, double h) : Shape(c), base(b), height(h) {}
    
    void draw() override {
        cout << "Drawing a " << color << " triangle with base " << base << " and height " << height << endl;
    }
    
    double area() override {
        return 0.5 * base * height;
    }
};

int main() {
    // Create a vector of Shape pointers
    vector<Shape*> shapes;
    
    // Add different types of shapes to the vector
    shapes.push_back(new Circle("red", 5));
    shapes.push_back(new Rectangle("blue", 4, 6));
    shapes.push_back(new Triangle("green", 3, 7));
    
    // Draw all shapes and print their areas
    cout << "Shapes in the drawing application:" << endl;
    for (auto& shape : shapes) {
        shape->draw();
        cout << "Area: " << shape->area() << " square units" << endl;
        cout << "--------------------------" << endl;
    }
    
    // Free allocated memory
    for (auto& shape : shapes) {
        delete shape;
    }
    
    return 0;
}

Output:

Shapes in the drawing application:
Drawing a red circle with radius 5
Area: 78.5397 square units
--------------------------
Drawing a blue rectangle with width 4 and height 6
Area: 24 square units
--------------------------
Drawing a green triangle with base 3 and height 7
Area: 10.5 square units
--------------------------

In this example, we have a base class Shape and three derived classes: Circle, Rectangle, and Triangle. We create a vector of Shape pointers that hold objects of different derived classes. When we call draw() and area() on each pointer, the appropriate version of the function is called based on the actual type of the object being pointed to, demonstrating runtime polymorphism.

Benefits of Polymorphism

  1. Code Reusability: Write code that can work with objects of multiple types through a common interface.
  2. Flexibility: Enable changes in the derived classes without affecting the base class code.
  3. Extensibility: Easily add new types that work with existing code.
  4. Maintainability: Simplify code structure by treating related objects uniformly.
  5. Abstraction: Allow focusing on what an object does rather than how it does it.

Common Use Cases for Polymorphism

  1. User Interface Elements: Buttons, text fields, checkboxes that all need to be drawn and respond to events.
  2. File Systems: Different types of files that can be opened, read, and written to.
  3. Game Objects: Characters, items, and obstacles that need to be updated and rendered.
  4. Database Connectors: Different database systems with a common interface.
  5. Payment Processing: Various payment methods with a unified checkout process.

Best Practices

  1. Use Virtual Destructors: Always use virtual destructors in the base class when using polymorphism.
  2. Avoid Deep Inheritance Hierarchies: Keep inheritance hierarchies shallow to improve maintainability.
  3. Prefer Composition Over Inheritance: Use composition when possible to achieve better flexibility.
  4. Use the override Keyword: Always use the override keyword for overridden functions to catch errors.
  5. Consider Pure Virtual Functions: Use pure virtual functions when the base class doesn’t have a meaningful implementation.

Summary

Polymorphism is a powerful concept in object-oriented programming that allows objects of different types to be treated as objects of a common type. C++ supports both compile-time polymorphism (function overloading, operator overloading, templates) and runtime polymorphism (virtual functions, function overriding).

Runtime polymorphism is particularly important as it enables you to write code that works with objects of many different types through a common interface, making your code more flexible, maintainable, and extensible.