Polymorphism Categories
● Static polymorphism: Achieved through function overloading and operator overloading, reusing the function name.
● Dynamic polymorphism: Achieved through derived classes and virtual functions, enabling runtime polymorphism.
The key difference:
● In static polymorphism, the function address is bound early – determined at compile time.
● In dynamic polymorphism, the function address is bound late – determined at runtime.
#include <iostream>
using namespace std;
class Creature {
public:
// The vocalize function is virtual.
// Adding the virtual keyword means the compiler cannot resolve the function call at compile time.
virtual void vocalize() {
cout << "Creature makes a sound" << endl;
}
};
class Feline : public Creature {
public:
void vocalize() override { // override specifier for clarity
cout << "Meow" << endl;
}
};
class Canine : public Creature {
public:
void vocalize() override {
cout << "Bark" << endl;
}
};
// We want the appropriate object's function to be called.
// If the function address is fixed at compile time → static binding (early binding).
// If it is resolved at runtime → dynamic binding (late binding).
// The parameter must be a reference (or pointer) to enable dynamic binding.
void makeSound(Creature& creature) {
creature.vocalize();
}
int main() {
Feline cat;
makeSound(cat); // outputs "Meow"
Canine dog;
makeSound(dog); // outputs "Bark"
return 0;
}</iostream>
In makeSound, the parameter is Creature& – a reference to the base class. If we removed the reference and passed by value, the function would always call Creature::vocalize() (static binding), regardlesss of the actual object passed. By using a reference (or pointer), the call is resolved at runtime based on the actual type of the object. This is dynamic binding.
Summary of requirements for polymorphism:
● An inheritance relationship must exist.
● The derived class must override a virtual function of the base class.
Usage condition for polymorphism:
● A base-class pointer or reference must point to (or refer to) a derived-class object.
Overriding: the return type, function name, and parameter list must be exactly the same.
Polymorphism Example: A Simple Calculator
#include <iostream>
using namespace std;
// Non-polymorphic implementation
class Calculator {
public:
int compute(const string& op) {
if (op == "+") {
return valueA + valueB;
} else if (op == "-") {
return valueA - valueB;
} else if (op == "*") {
return valueA * valueB;
}
// Adding a new operation requires modifying this source code.
return 0;
}
int valueA;
int valueB;
};
void testNonPoly() {
Calculator calc;
calc.valueA = 10;
calc.valueB = 5;
cout << calc.valueA << " + " << calc.valueB << " = " << calc.compute("+") << endl;
cout << calc.valueA << " - " << calc.valueB << " = " << calc.compute("-") << endl;
cout << calc.valueA << " * " << calc.valueB << " = " << calc.compute("*") << endl;
}
// Polymorphic implementation
// Abstract base class for a binary operation
// Advantages: clearer organization, high readability, easier extension and maintenance.
class AbstractOperation {
public:
virtual int execute() = 0;
int operandA;
int operandB;
};
class Addition : public AbstractOperation {
public:
int execute() override {
return operandA + operandB;
}
};
class Subtraction : public AbstractOperation {
public:
int execute() override {
return operandA - operandB;
}
};
class Multiplication : public AbstractOperation {
public:
int execute() override {
return operandA * operandB;
}
};
void testPoly() {
AbstractOperation* op = new Addition();
op->operandA = 10;
op->operandB = 5;
cout << op->operandA << " + " << op->operandB << " = " << op->execute() << endl;
delete op; // clean up
op = new Subtraction();
op->operandA = 10;
op->operandB = 5;
cout << op->operandA << " - " << op->operandB << " = " << op->execute() << endl;
delete op;
op = new Multiplication();
op->operandA = 10;
op->operandB = 5;
cout << op->operandA << " * " << op->operandB << " = " << op->execute() << endl;
delete op;
}
int main() {
// testNonPoly();
testPoly();
return 0;
}</iostream>
Pure Virtual Functions and Abstract Classes
In polymorphic designs, the base class implementation of a virtual function is often meaningless; the actual work is done by the derived classes. Such functions can be declared as pure virtual functions.
Syntax: virtual ReturnType functionName(parameters) = 0;
A class containing atleast one pure virtual function becomes an abstract class.
Characteristics of abstract classes:
● Objects cannot be instantiated directly.
● Derived classes must override all pure virtual functions; otherwise, they also become abstract.
#include <iostream>
using namespace std;
class Base {
public:
// Pure virtual function makes this an abstract class.
// Abstract classes cannot be instantiated.
// Derived classes must override this function, or they remain abstract.
virtual void process() = 0;
};
class Derived : public Base {
public:
void process() override {
cout << "process() invoked" << endl;
}
};
int main() {
Base* ptr = nullptr;
// ptr = new Base; // Error: cannot instantiate abstract class
ptr = new Derived();
ptr->process();
delete ptr; // free memory
return 0;
}</iostream>
Practical Use Case: Preparing Beverages
#include <iostream>
using namespace std;
// Abstract class representing a beverage preparation process
class AbstractBeverage {
public:
virtual void heatWater() = 0;
virtual void infuse() = 0;
virtual void pourIntoCup() = 0;
virtual void addCondiments() = 0;
// Template method defining the overall procedure
void prepare() {
heatWater();
infuse();
pourIntoCup();
addCondiments();
}
};
// Coffee preparation
class Coffee : public AbstractBeverage {
public:
void heatWater() override {
cout << "Heating filtered water!" << endl;
}
void infuse() override {
cout << "Brewing coffee grounds!" << endl;
}
void pourIntoCup() override {
cout << "Pouring coffee into the cup!" << endl;
}
void addCondiments() override {
cout << "Adding milk and sugar!" << endl;
}
};
// Tea preparation
class Tea : public AbstractBeverage {
public:
void heatWater() override {
cout << "Boiling tap water!" << endl;
}
void infuse() override {
cout << "Steeping tea leaves!" << endl;
}
void pourIntoCup() override {
cout << "Pouring tea into the cup!" << endl;
}
void addCondiments() override {
cout << "Adding honey and lemon!" << endl;
}
};
// Client function using the base-class pointer
void serveBeverage(AbstractBeverage* beverage) {
beverage->prepare();
delete beverage;
}
int main() {
serveBeverage(new Coffee());
serveBeverage(new Tea());
return 0;
}</iostream>
Virtual Destructors and Pure Virtual Destructors
When a derived class allocates memory on the heap, deleting an object through a base-class pointer may not invoke the derived class destructor, causing memory leaks.
Solution: make the base class destructor virtual or pure virtual.
Commonalities:
● Both enable proper cleanup of derived objects when deleted through a base pointer.
● Both require an actual function definition (even pure virtual destructors need an implementation).
Differences:
● A class with a pure virtual destructor is abstract and cannot be instantiated.
Virtual destructor syntax:
virtual ~ClassName() {}
Pure virtual destructor syntax:
virtual ~ClassName() = 0;
ClassName::~ClassName() {} // definition required outside the class
#include <iostream>
using namespace std;
class Creature {
public:
Creature() {
cout << "Creature constructor called!" << endl;
}
virtual void speak() = 0; // pure virtual function
// Pure virtual destructor – makes Creature abstract
virtual ~Creature() = 0;
};
// Definition of the pure virtual destructor
Creature::~Creature() {
cout << "Creature pure virtual destructor called!" << endl;
}
class Feline : public Creature {
public:
Feline(string name) {
cout << "Feline constructor called!" << endl;
petName = new string(name);
}
void speak() override {
cout << *petName << " the cat says meow!" << endl;
}
~Feline() {
cout << "Feline destructor called!" << endl;
if (petName != nullptr) {
delete petName;
petName = nullptr;
}
}
private:
string* petName; // heap-allocated memory in derived class
};
int main() {
Creature* creature = new Feline("Tom");
creature->speak();
// Deleting through base pointer: if destructor were not virtual,
// the Feline destructor wouldn't be called, causing a memory leak.
delete creature;
return 0;
}</iostream>
Key takeaways:
● Virtual (or pure virtual) destructors ensure proper cleanup when deleting derived objects through a base pointer.
● If a derived class does not allocate heap resources, a virtual destructor is not strictly required.
● A class with a pure virtual destructor is abstract and cannot be instantiated directly.