Polymorphism Concept

What is Polymorphism?

Polymorphism is a core concept in object-oriented programming that allows objects of different classes to be treated as objects of a common base class. The word “polymorphism” comes from Greek, meaning “many forms.” In programming, it means that a single interface can represent different underlying forms (data types or classes).

In simpler terms, polymorphism allows different objects to respond to the same method call in different ways, based on their specific implementations.

Why is Polymorphism Important?

Polymorphism provides several key benefits:

  1. Simplifies Code: Use the same interface for different objects
  2. Increases Flexibility: Change object behavior without changing the code that uses the objects
  3. Enables Extensibility: Add new classes without modifying existing code
  4. Supports Loose Coupling: Reduces dependencies between different parts of the code
  5. Promotes Code Reuse: Share common functionality while customizing specific behaviors

Types of Polymorphism in C++

C++ supports two main types of polymorphism:

1. Compile-time Polymorphism (Static Polymorphism)

This type of polymorphism is resolved during compilation.

a. Function Overloading

Function overloading occurs when multiple functions have the same name but different parameters:

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

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

void display(std::string text) {
    std::cout << "String: " << text << std::endl;
}

int main() {
    display(5);          // Calls display(int)
    display(5.5);        // Calls display(double)
    display("Hello");    // Calls display(std::string)
    return 0;
}

b. Operator Overloading

Operator overloading allows operators to be redefined for user-defined types:

class Complex {
private:
    double real, imag;
    
public:
    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;
    }
    
    void display() {
        std::cout << real << " + " << imag << "i" << std::endl;
    }
};

int main() {
    Complex c1(3, 2), c2(1, 4);
    Complex c3 = c1 + c2;  // Using overloaded + operator
    c3.display();          // Outputs: 4 + 6i
    return 0;
}

2. Run-time Polymorphism (Dynamic Polymorphism)

This type of polymorphism is resolved during program execution.

a. Method Overriding and Virtual Functions

Method overriding occurs when a derived class provides a specific implementation for a method already defined in its base class:

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

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

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

int main() {
    Animal* animal1 = new Dog();
    Animal* animal2 = new Cat();
    
    animal1->makeSound();  // Outputs: Dog barks: Woof! Woof!
    animal2->makeSound();  // Outputs: Cat meows: Meow! Meow!
    
    delete animal1;
    delete animal2;
    
    return 0;
}

Key Elements of Polymorphism in C++

Virtual Functions

Virtual functions enable runtime polymorphism in C++. When a method is declared as virtual in a base class, C++ determines which version of the method to call based on the actual type of the object, not the type of the pointer or reference.

class Shape {
public:
    virtual double area() {
        return 0.0;
    }
    
    virtual void draw() {
        std::cout << "Drawing a generic shape" << std::endl;
    }
};

class Circle : public Shape {
private:
    double radius;
    
public:
    Circle(double r) : radius(r) {}
    
    double area() override {
        return 3.14159 * radius * radius;
    }
    
    void draw() override {
        std::cout << "Drawing a circle" << std::endl;
    }
};

class Rectangle : public Shape {
private:
    double width, height;
    
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    
    double area() override {
        return width * height;
    }
    
    void draw() override {
        std::cout << "Drawing a rectangle" << std::endl;
    }
};

Pure Virtual Functions and Abstract Classes

A pure virtual function is declared with = 0 and has no implementation in the base class. A class with at least one pure virtual function is called an abstract class and cannot be instantiated.

class Shape {
public:
    // Pure virtual function
    virtual double area() = 0;
    virtual void draw() = 0;
    
    // Regular method
    void displayArea() {
        std::cout << "Area: " << area() << std::endl;
    }
};

// Shape is an abstract class and cannot be instantiated:
// Shape shape; // Error!

class Circle : public Shape {
private:
    double radius;
    
public:
    Circle(double r) : radius(r) {}
    
    double area() override {
        return 3.14159 * radius * radius;
    }
    
    void draw() override {
        std::cout << "Drawing a circle with radius " << radius << std::endl;
    }
};

Real-world Example: Drawing Application

Here’s a more complex example demonstrating polymorphism in a simple drawing application:

#include <iostream>
#include <vector>
#include <string>

class Shape {
protected:
    int x, y;
    std::string color;
    
public:
    Shape(int xPos, int yPos, std::string shapeColor) 
        : x(xPos), y(yPos), color(shapeColor) {}
    
    virtual ~Shape() {}  // Virtual destructor for proper cleanup
    
    // Pure virtual functions
    virtual double area() = 0;
    virtual double perimeter() = 0;
    virtual void draw() = 0;
    
    // Common method for all shapes
    void move(int newX, int newY) {
        x = newX;
        y = newY;
        std::cout << "Shape moved to position (" << x << ", " << y << ")" << std::endl;
    }
    
    std::string getColor() {
        return color;
    }
};

class Circle : public Shape {
private:
    double radius;
    
public:
    Circle(int xPos, int yPos, std::string color, double r) 
        : Shape(xPos, yPos, color), radius(r) {}
    
    double area() override {
        return 3.14159 * radius * radius;
    }
    
    double perimeter() override {
        return 2 * 3.14159 * radius;
    }
    
    void draw() override {
        std::cout << "Drawing a " << getColor() << " circle at (" 
                  << x << ", " << y << ") with radius " << radius << std::endl;
    }
};

class Rectangle : public Shape {
private:
    double width, height;
    
public:
    Rectangle(int xPos, int yPos, std::string color, double w, double h) 
        : Shape(xPos, yPos, color), width(w), height(h) {}
    
    double area() override {
        return width * height;
    }
    
    double perimeter() override {
        return 2 * (width + height);
    }
    
    void draw() override {
        std::cout << "Drawing a " << getColor() << " rectangle at (" 
                  << x << ", " << y << ") with width " << width 
                  << " and height " << height << std::endl;
    }
};

class Triangle : public Shape {
private:
    double side1, side2, side3;
    
public:
    Triangle(int xPos, int yPos, std::string color, double s1, double s2, double s3) 
        : Shape(xPos, yPos, color), side1(s1), side2(s2), side3(s3) {}
    
    double area() override {
        // Using Heron's formula
        double s = (side1 + side2 + side3) / 2;
        return sqrt(s * (s - side1) * (s - side2) * (s - side3));
    }
    
    double perimeter() override {
        return side1 + side2 + side3;
    }
    
    void draw() override {
        std::cout << "Drawing a " << getColor() << " triangle at (" 
                  << x << ", " << y << ") with sides " << side1 
                  << ", " << side2 << ", and " << side3 << std::endl;
    }
};

// Drawing manager that works with any shape
class DrawingManager {
private:
    std::vector<Shape*> shapes;
    
public:
    void addShape(Shape* shape) {
        shapes.push_back(shape);
    }
    
    void drawAll() {
        for (Shape* shape : shapes) {
            shape->draw();
        }
    }
    
    void printAreas() {
        for (int i = 0; i < shapes.size(); i++) {
            std::cout << "Shape " << i + 1 << " area: " << shapes[i]->area() << std::endl;
        }
    }
    
    ~DrawingManager() {
        // Clean up the shapes
        for (Shape* shape : shapes) {
            delete shape;
        }
        shapes.clear();
    }
};

int main() {
    DrawingManager manager;
    
    // Add different shapes to the manager
    manager.addShape(new Circle(10, 10, "red", 5.0));
    manager.addShape(new Rectangle(20, 20, "blue", 8.0, 4.0));
    manager.addShape(new Triangle(30, 30, "green", 3.0, 4.0, 5.0));
    
    // Draw all shapes
    std::cout << "Drawing all shapes:" << std::endl;
    manager.drawAll();
    
    // Print all areas
    std::cout << "\nAreas of all shapes:" << std::endl;
    manager.printAreas();
    
    return 0;
}

Benefits of Using Polymorphism

  1. Code Flexibility: Handle objects of different classes through a common interface
  2. Extensibility: Add new derived classes without changing existing code
  3. Maintainability: Centralize common behavior in base classes
  4. Reusability: Reuse code for similar objects while allowing for customization
  5. Loose Coupling: Reduce dependencies between different parts of the code

When to Use Different Types of Polymorphism

Use Compile-time Polymorphism When:

  • The behavior is determined by different parameter types
  • Performance is critical (no runtime overhead)
  • The set of types is fixed and known at compile time

Use Run-time Polymorphism When:

  • You need to work with objects of different types through a common interface
  • The exact type of object may not be known until runtime
  • You want to extend functionality by adding new derived classes without modifying existing code

Polymorphism vs. Overloading vs. Overriding

ConceptDescriptionType of Polymorphism
Function OverloadingMultiple functions with the same name but different parametersCompile-time
Operator OverloadingRedefining operators for user-defined typesCompile-time
Method OverridingProviding a specific implementation for a method defined in a base classRun-time

Conclusion

Polymorphism is a powerful concept in object-oriented programming that allows for flexible, extensible, and maintainable code. By treating different objects through a common interface, you can write code that works with a variety of types without knowing the specific details of each type. This enables you to easily add new functionality by creating new derived classes without modifying existing code.

In C++, you can use both compile-time polymorphism (through function and operator overloading) and run-time polymorphism (through virtual functions and method overriding), choosing the appropriate approach based on your specific requirements and constraints.