Compile-Time Polymorphism

Compile-time polymorphism, also known as static polymorphism, is a type of polymorphism that is resolved during the compilation of the program rather than at runtime. This form of polymorphism enables a program to process objects differently based on their data type, attributes, or the number and types of arguments passed to methods.

Understanding Compile-Time Polymorphism

In C++, compile-time polymorphism is implemented through two main mechanisms:

  1. Function Overloading
  2. Operator Overloading

The key characteristic of compile-time polymorphism is that the decision about which function or operator to call is made by the compiler during the compilation phase, based on the function signature (number and types of parameters) or the types involved in an operation.

Function Overloading

Function overloading is a feature in C++ that allows multiple functions to have the same name but different parameters. The compiler determines which function to call based on the number, types, and order of the arguments in the function call.

Basic Example

#include <iostream>
using namespace std;

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

// Function with one float parameter
void display(float num) {
    cout << "Float value: " << num << endl;
}

// Function with two integer parameters
void display(int num1, int num2) {
    cout << "Sum of integers: " << (num1 + num2) << endl;
}

// Function with a string parameter
void display(string text) {
    cout << "String value: " << text << endl;
}

int main() {
    display(10);           // Calls display(int)
    display(10.5f);        // Calls display(float)
    display(5, 15);        // Calls display(int, int)
    display("Hello");      // Calls display(string)
    
    return 0;
}

How Function Overloading Works

  1. Name Lookup: The compiler first identifies all functions with the given name.
  2. Overload Resolution: The compiler then determines the best match for the function call based on:
    • Number of arguments
    • Types of arguments
    • Conversion sequences (if needed)
  3. Selection: The compiler selects the most appropriate function.

Rules for Function Overloading

  1. Different Parameter Types: Functions can be overloaded by having different parameter types.

    void func(int x);
    void func(double x);
  2. Different Number of Parameters: Functions can be overloaded with a different number of parameters.

    void func(int x);
    void func(int x, int y);
  3. Different Order of Parameters: Functions can be overloaded by changing the order of parameters.

    void func(int x, float y);
    void func(float x, int y);
  4. Default Arguments: Functions with default arguments do not constitute a valid overload if they have the same parameter signature.

    // This is NOT a valid overload - it will cause a compilation error
    void func(int x);
    void func(int x = 10);  // Error: redefinition of 'func'
  5. Return Type: Functions cannot be overloaded based solely on return type.

    // This is NOT a valid overload - it will cause a compilation error
    int func(int x);
    double func(int x);  // Error: different return type is not enough

Advanced Function Overloading Example

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

class Calculator {
public:
    // Integer addition
    int add(int a, int b) {
        cout << "Adding two integers" << endl;
        return a + b;
    }
    
    // Float addition
    float add(float a, float b) {
        cout << "Adding two floats" << endl;
        return a + b;
    }
    
    // Three integer addition
    int add(int a, int b, int c) {
        cout << "Adding three integers" << endl;
        return a + b + c;
    }
    
    // String concatenation
    string add(string a, string b) {
        cout << "Concatenating two strings" << endl;
        return a + b;
    }
    
    // Integer and float addition
    float add(int a, float b) {
        cout << "Adding an integer and a float" << endl;
        return a + b;
    }
    
    // Float and integer addition (different order)
    float add(float a, int b) {
        cout << "Adding a float and an integer" << endl;
        return a + b;
    }
};

int main() {
    Calculator calc;
    
    cout << "Result: " << calc.add(5, 10) << endl;                 // Calls int add(int, int)
    cout << "Result: " << calc.add(5.5f, 10.5f) << endl;           // Calls float add(float, float)
    cout << "Result: " << calc.add(5, 10, 15) << endl;             // Calls int add(int, int, int)
    cout << "Result: " << calc.add(string("Hello"), string(" World")) << endl; // Calls string add(string, string)
    cout << "Result: " << calc.add(5, 10.5f) << endl;              // Calls float add(int, float)
    cout << "Result: " << calc.add(5.5f, 10) << endl;              // Calls float add(float, int)
    
    return 0;
}

Function Overloading with Member Functions

Function overloading works the same way with member functions in classes. Member functions can be overloaded based on the number and types of parameters.

#include <iostream>
using namespace std;

class Shape {
private:
    string name;
    
public:
    // Default constructor
    Shape() : name("Unknown") {
        cout << "Creating a shape with no name" << endl;
    }
    
    // Parameterized constructor (also a form of overloading)
    Shape(string shapeName) : name(shapeName) {
        cout << "Creating a shape named " << name << endl;
    }
    
    // Calculate area with no parameters (default unit square)
    double area() {
        cout << "Calculating area of a unit square" << endl;
        return 1.0;
    }
    
    // Calculate area of a rectangle
    double area(double length, double width) {
        cout << "Calculating area of a rectangle" << endl;
        return length * width;
    }
    
    // Calculate area of a circle
    double area(double radius) {
        cout << "Calculating area of a circle" << endl;
        return 3.14159 * radius * radius;
    }
    
    // Calculate area of a triangle
    double area(double base, double height, bool isTriangle) {
        if (isTriangle) {
            cout << "Calculating area of a triangle" << endl;
            return 0.5 * base * height;
        } else {
            // Fall back to rectangle calculation if not a triangle
            return area(base, height);
        }
    }
    
    string getName() {
        return name;
    }
};

int main() {
    Shape defaultShape;
    cout << "Default shape name: " << defaultShape.getName() << endl;
    cout << "Default shape area: " << defaultShape.area() << endl << endl;
    
    Shape rectangle("Rectangle");
    cout << rectangle.getName() << " area: " << rectangle.area(5.0, 3.0) << endl << endl;
    
    Shape circle("Circle");
    cout << circle.getName() << " area: " << circle.area(2.5) << endl << endl;
    
    Shape triangle("Triangle");
    cout << triangle.getName() << " area: " << triangle.area(4.0, 3.0, true) << endl << endl;
    
    return 0;
}

Operator Overloading

Operator overloading is another form of compile-time polymorphism in C++ that allows operators such as +, -, *, /, etc., to have different implementations depending on the operands.

Basic Example

#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 + operator for Complex + Complex
    Complex operator+(const Complex& other) const {
        return Complex(real + other.real, imag + other.imag);
    }
    
    // Overload - operator for Complex - Complex
    Complex operator-(const Complex& other) const {
        return Complex(real - other.real, imag - other.imag);
    }
    
    // Overload * operator for Complex * Complex
    Complex operator*(const Complex& other) const {
        return Complex(
            real * other.real - imag * other.imag, 
            real * other.imag + imag * other.real
        );
    }
    
    // Display the complex number
    void display() const {
        cout << real;
        if (imag >= 0) cout << " + " << imag << "i";
        else cout << " - " << -imag << "i";
        cout << endl;
    }
};

int main() {
    Complex c1(3, 2);
    Complex c2(1, 4);
    
    cout << "c1 = "; c1.display();
    cout << "c2 = "; c2.display();
    
    Complex sum = c1 + c2;
    cout << "c1 + c2 = "; sum.display();
    
    Complex diff = c1 - c2;
    cout << "c1 - c2 = "; diff.display();
    
    Complex prod = c1 * c2;
    cout << "c1 * c2 = "; prod.display();
    
    return 0;
}

Overloading Different Types of Operators

  1. Unary Operators: Operators that work on a single operand (++, —, !, -, etc.)
#include <iostream>
using namespace std;

class Counter {
private:
    int count;
    
public:
    Counter(int c = 0) : count(c) {}
    
    // Prefix increment operator
    Counter& operator++() {
        ++count;
        return *this;
    }
    
    // Postfix increment operator
    Counter operator++(int) {
        Counter temp = *this;
        ++count;
        return temp;
    }
    
    // Unary minus operator
    Counter operator-() const {
        return Counter(-count);
    }
    
    // Not operator
    bool operator!() const {
        return count == 0;
    }
    
    // Display the counter value
    void display() const {
        cout << "Count: " << count << endl;
    }
    
    int getCount() const {
        return count;
    }
};

int main() {
    Counter c1(5);
    cout << "Initial: "; c1.display();
    
    // Prefix increment
    ++c1;
    cout << "After ++c1: "; c1.display();
    
    // Postfix increment
    Counter c2 = c1++;
    cout << "c2 (from c1++): "; c2.display();
    cout << "c1 after c1++: "; c1.display();
    
    // Unary minus
    Counter c3 = -c1;
    cout << "c3 (from -c1): "; c3.display();
    
    // Not operator
    Counter c4(0);
    if (!c4) {
        cout << "c4 is zero" << endl;
    }
    
    return 0;
}
  1. Binary Operators: Operators that work on two operands (+, -, *, /, ==, !=, <, >, etc.)
#include <iostream>
#include <string>
using namespace std;

class String {
private:
    string data;
    
public:
    String(const string& s = "") : data(s) {}
    
    // Overload + operator for concatenation
    String operator+(const String& other) const {
        return String(data + other.data);
    }
    
    // Overload == operator for equality comparison
    bool operator==(const String& other) const {
        return data == other.data;
    }
    
    // Overload != operator for inequality comparison
    bool operator!=(const String& other) const {
        return data != other.data;
    }
    
    // Overload < operator for less than comparison
    bool operator<(const String& other) const {
        return data < other.data;
    }
    
    // Overload > operator for greater than comparison
    bool operator>(const String& other) const {
        return data > other.data;
    }
    
    // Overload [] operator for indexing
    char& operator[](size_t index) {
        return data[index];
    }
    
    // Const version of the [] operator
    const char& operator[](size_t index) const {
        return data[index];
    }
    
    // Display the string
    void display() const {
        cout << "String: " << data << endl;
    }
    
    string getData() const {
        return data;
    }
};

int main() {
    String s1("Hello");
    String s2("World");
    
    cout << "s1: " << s1.getData() << endl;
    cout << "s2: " << s2.getData() << endl;
    
    // Concatenation
    String s3 = s1 + s2;
    cout << "s1 + s2: " << s3.getData() << endl;
    
    // Comparison
    if (s1 == s1) {
        cout << "s1 equals s1" << endl;
    }
    
    if (s1 != s2) {
        cout << "s1 is not equal to s2" << endl;
    }
    
    if (s1 < s2) {
        cout << "s1 is less than s2" << endl;
    }
    
    if (s2 > s1) {
        cout << "s2 is greater than s1" << endl;
    }
    
    // Indexing
    cout << "First char of s1: " << s1[0] << endl;
    
    // Modify through indexing
    s1[0] = 'J';
    cout << "Modified s1: " << s1.getData() << endl;
    
    return 0;
}
  1. Stream Operators: Can be overloaded to customize input/output operations
#include <iostream>
using namespace std;

class Person {
private:
    string name;
    int age;
    
public:
    Person(const string& n = "", int a = 0) : name(n), age(a) {}
    
    // Overload << operator for output
    friend ostream& operator<<(ostream& os, const Person& person);
    
    // Overload >> operator for input
    friend istream& operator>>(istream& is, Person& person);
};

// Implementation of << operator overload
ostream& operator<<(ostream& os, const Person& person) {
    os << "Name: " << person.name << ", Age: " << person.age;
    return os;
}

// Implementation of >> operator overload
istream& operator>>(istream& is, Person& person) {
    cout << "Enter name: ";
    is >> person.name;
    cout << "Enter age: ";
    is >> person.age;
    return is;
}

int main() {
    Person p1("John Doe", 30);
    cout << "Person 1: " << p1 << endl;
    
    Person p2;
    cout << "\nEnter details for Person 2:" << endl;
    cin >> p2;
    
    cout << "\nPerson 2: " << p2 << endl;
    
    return 0;
}
  1. Assignment Operators: Can be overloaded to customize how values are assigned to objects
#include <iostream>
using namespace std;

class DynamicArray {
private:
    int* data;
    size_t size;
    
public:
    // Constructor
    DynamicArray(size_t s = 0) : size(s) {
        if (size > 0) {
            data = new int[size]();  // Initialize with zeros
        } else {
            data = nullptr;
        }
    }
    
    // Destructor
    ~DynamicArray() {
        delete[] data;
    }
    
    // Copy constructor
    DynamicArray(const DynamicArray& other) : size(other.size) {
        if (size > 0) {
            data = new int[size];
            for (size_t i = 0; i < size; ++i) {
                data[i] = other.data[i];
            }
        } else {
            data = nullptr;
        }
    }
    
    // Assignment operator
    DynamicArray& operator=(const DynamicArray& other) {
        if (this != &other) {  // Self-assignment check
            // Delete old data
            delete[] data;
            
            // Copy new data
            size = other.size;
            if (size > 0) {
                data = new int[size];
                for (size_t i = 0; i < size; ++i) {
                    data[i] = other.data[i];
                }
            } else {
                data = nullptr;
            }
        }
        return *this;
    }
    
    // Subscript operator
    int& operator[](size_t index) {
        if (index >= size) {
            throw out_of_range("Index out of bounds");
        }
        return data[index];
    }
    
    // Const version of subscript operator
    const int& operator[](size_t index) const {
        if (index >= size) {
            throw out_of_range("Index out of bounds");
        }
        return data[index];
    }
    
    // Display the array
    void display() const {
        cout << "Array [";
        for (size_t i = 0; i < size; ++i) {
            cout << data[i];
            if (i < size - 1) cout << ", ";
        }
        cout << "]" << endl;
    }
    
    size_t getSize() const {
        return size;
    }
};

int main() {
    DynamicArray arr1(5);
    for (size_t i = 0; i < arr1.getSize(); ++i) {
        arr1[i] = i * 10;
    }
    
    cout << "arr1: ";
    arr1.display();
    
    // Copy construction
    DynamicArray arr2(arr1);
    cout << "arr2 (copy of arr1): ";
    arr2.display();
    
    // Modify arr2
    arr2[2] = 999;
    cout << "Modified arr2: ";
    arr2.display();
    cout << "arr1 (unchanged): ";
    arr1.display();
    
    // Assignment
    DynamicArray arr3(3);
    cout << "arr3 (before assignment): ";
    arr3.display();
    
    arr3 = arr2;
    cout << "arr3 (after arr3 = arr2): ";
    arr3.display();
    
    // Modify arr3
    arr3[0] = 777;
    cout << "Modified arr3: ";
    arr3.display();
    cout << "arr2 (unchanged): ";
    arr2.display();
    
    return 0;
}