Contents

Polymorphism

Function Overriding in C++

Polymorphism, derived from the term meaning “many forms,” refers to the ability of an entity to take on different behaviors or forms depending on the context. In Object-Oriented Programming (OOP), polymorphism is a core feature that enables a single action to be performed in various ways, depending on the object that invokes it. A common real-world example is a person who assumes different roles simultaneously, such as being a parent, an employee, and a friend. Similarly, polymorphism allows objects of different classes to respond uniquely to the same function call, making it a fundamental concept in OOP.

Types of Polymorphism:

1. Compile-Time Polymorphism (also called early binding or static polymorphism)
2. Run-Time Polymorphism (also known as late binding or dynamic polymorphism)

1. Compile-Time Polymorphism:

This type of polymorphism is achieved using function overloading or operator overloading.

A. Function Overloading:

In function overloading, multiple functions share the same name but differ in the number or type of parameters. Depending on the arguments passed, the appropriate function is selected at compile time.

Example of Function Overloading in C++:

				
					#include <iostream>
using namespace std;

class Example {
public:
    void display(int a) {
        cout << "Integer: " << a << endl;
    }

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

    void display(int a, int b) {
        cout << "Two Integers: " << a << " and " << b << endl;
    }
};

int main() {
    Example obj;

    obj.display(5);        // Calls the function with an integer argument
    obj.display(3.14);     // Calls the function with a double argument
    obj.display(7, 8);     // Calls the function with two integer arguments
}

				
			

Output:

				
					Integer: 5
Double: 3.14
Two Integers: 7 and 8

				
			

In this example, the display function behaves differently based on the type and number of arguments passed to it, demonstrating compile-time polymorphism.

B. Operator Overloading:

Operator overloading allows you to redefine the meaning of operators for user-defined types. For instance, the + operator can be overloaded to work with objects like complex numbers.

Example of Operator Overloading in C++:

				
					#include <iostream>
using namespace std;

class Complex {
private:
    int real, imag;

public:
    Complex(int r = 0, int i = 0) : real(r), imag(i) {}

    // Overloading the '+' operator for complex numbers
    Complex operator+(const Complex& obj) {
        Complex res;
        res.real = real + obj.real;
        res.imag = imag + obj.imag;
        return res;
    }

    void display() const {
        cout << real << " + i" << imag << endl;
    }
};

int main() {
    Complex c1(3, 4), c2(1, 2);
    Complex c3 = c1 + c2;  // Calls overloaded '+' operator
    c3.display();
}

				
			

Output:

				
					4 + i6
				
			

Here, the + operator is overloaded to add complex numbers, showing compile-time polymorphism using operator overloading.

2. Run-Time Polymorphism

This type of polymorphism is accomplished using function overriding, where a derived class provides a specific implementation of a function that is already defined in its base class. This allows the correct function to be called based on the object type at runtime.

A. Function Overriding:

When a derived class defines a function that overrides a function in its base class, the base function is “overridden.” The decision to call the base or derived class function is made at runtime.

Example of Function Overriding in C++:

				
					#include <iostream>
using namespace std;

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

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

int main() {
    Animal* animalPtr;
    Dog dog;
    
    animalPtr = &dog;
    animalPtr->sound();  // Calls the overridden method in the Dog class
}

				
			

Output:

				
					Dog barks
				
			

In this example, the sound method is overridden in the Dog class, and the function call is resolved at runtime based on the actual object type.

B. Virtual Function:

A virtual function in a base class allows derived classes to override it. When a base class pointer points to a derived class object, the overridden function is called, enabling run-time polymorphism.

Example of Virtual Function in C++:

				
					#include <iostream>
using namespace std;

class Base {
public:
    virtual void show() {
        cout << "Base class show function" << endl;
    }

    void display() {
        cout << "Base class display function" << endl;
    }
};

class Derived : public Base {
public:
    void show() override {
        cout << "Derived class show function" << endl;
    }

    void display() {
        cout << "Derived class display function" << endl;
    }
};

int main() {
    Base* basePtr;
    Derived derivedObj;

    basePtr = &derivedObj;

    basePtr->show();     // Calls the overridden show() in Derived class
    basePtr->display();  // Calls the display() in Base class
}

				
			

Output:

				
					Derived class show function
Base class display function
				
			

Virtual Functions and Runtime Polymorphism in C++

A virtual function is a member function that is declared in a base class using the virtual keyword and redefined (overridden) in a derived class. It enables late binding, meaning that the compiler decides which function to call at runtime based on the object type, not the pointer type. This is a key feature of runtime polymorphism.

The term “Polymorphism” refers to the ability to take many forms. When applied to object-oriented programming (OOP), polymorphism allows different classes related by inheritance to behave in various ways under a common interface. Breaking it down, Poly means “many” and morphism means “forms”, implying different behaviors in different contexts.

Concept of Virtual Functions

Virtual functions in C++ allow a base class pointer to invoke derived class functions, achieving runtime polymorphism. The base class’s function is overridden by the derived class function, but the call is resolved at runtime. A key point to remember is that virtual functions aren’t allowed in constructors since the object construction follows a “bottom-up” approach.

Example without Virtual Functions

Let’s look at a simple example where we calculate the area of different shapes without using virtual functions:

				
					#include <iostream>
using namespace std;

class Shape {
public:
    Shape(int l, int w) : length(l), width(w) {}
    int getArea() {
        cout << "This is a call to the base class area\n";
        return 1;
    }

protected:
    int length, width;
};

class Square : public Shape {
public:
    Square(int l = 0, int w = 0) : Shape(l, w) {}
    int getArea() {
        cout << "Square area: " << length * width << endl;
        return length * width;
    }
};

class Rectangle : public Shape {
public:
    Rectangle(int l = 0, int w = 0) : Shape(l, w) {}
    int getArea() {
        cout << "Rectangle area: " << length * width << endl;
        return length * width;
    }
};

int main() {
    Shape* s;
    Square sq(4, 4);
    Rectangle rec(4, 5);

    s = &sq;
    s->getArea();  // Calls base class method
    s = &rec;
    s->getArea();  // Calls base class method

    return 0;
}

				
			

Output:

				
					This is a call to the base class area
This is a call to the base class area

				
			

Here, despite pointing to derived class objects, the getArea() function of the base class is called due to static binding.

Example with Virtual Functions

Now, let’s modify the code to include virtual functions for achieving runtime polymorphism:

				
					#include <iostream>
using namespace std;

class Shape {
public:
    virtual void calculateArea() {
        cout << "Calculating area in base class" << endl;
    }
    virtual ~Shape() {
        cout << "Shape destructor called" << endl;
    }
};

class Square : public Shape {
public:
    int side;
    Square(int s) : side(s) {}

    void calculateArea() override {
        cout << "Square area: " << side * side << endl;
    }

    ~Square() {
        cout << "Square destructor called" << endl;
    }
};

class Rectangle : public Shape {
public:
    int width, height;
    Rectangle(int w, int h) : width(w), height(h) {}

    void calculateArea() override {
        cout << "Rectangle area: " << width * height << endl;
    }

    ~Rectangle() {
        cout << "Rectangle destructor called" << endl;
    }
};

int main() {
    Shape* shape;
    Square sq(4);
    Rectangle rec(5, 6);

    shape = &sq;
    shape->calculateArea();  // Calls Square's method

    shape = &rec;
    shape->calculateArea();  // Calls Rectangle's method

    return 0;
}

				
			

Output:

				
					Square area: 16
Rectangle area: 30
				
			

In this example, the calculateArea() function calls are resolved at runtime, based on the actual object being pointed to. This demonstrates the power of virtual functions and runtime polymorphism.

How Virtual Functions are Resolved at Runtime

To resolve virtual functions at runtime, the compiler typically uses two mechanisms:

  • vtable: A table of function pointers maintained per class.
  • vptr: A pointer to the vtable, maintained per object instance.

At runtime, the vptr helps the compiler determine which function (base or derived) to invoke by consulting the appropriate vtable.

Practical Use of Virtual Functions: Employee Management System

Consider a scenario of managing employees in an organization. Suppose you have a base class Employee and derived classes like Manager and Engineer. Each class may override virtual functions like raiseSalary() or promote(). The virtual mechanism allows these functions to be called based on the actual type of employee without worrying about their specific implementation.

				
					class Employee {
public:
    virtual void raiseSalary() {
        // General salary raise logic
    }
    virtual void promote() {
        // General promotion logic
    }
    virtual ~Employee() {}
};

class Manager : public Employee {
public:
    void raiseSalary() override {
        // Manager-specific raise logic
    }
    void promote() override {
        // Manager-specific promotion logic
    }
};

class Engineer : public Employee {
public:
    void raiseSalary() override {
        // Engineer-specific raise logic
    }
    void promote() override {
        // Engineer-specific promotion logic
    }
};

void applyRaise(Employee* employees[], int size) {
    for (int i = 0; i < size; ++i) {
        employees[i]->raiseSalary();
    }
}