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:
- Function Overloading
- 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
- Name Lookup: The compiler first identifies all functions with the given name.
- 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)
- Selection: The compiler selects the most appropriate function.
Rules for Function Overloading
-
Different Parameter Types: Functions can be overloaded by having different parameter types.
void func(int x); void func(double x); -
Different Number of Parameters: Functions can be overloaded with a different number of parameters.
void func(int x); void func(int x, int y); -
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); -
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' -
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
- 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;
}
- 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;
}
- 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;
}
- 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;
}