65% complete
Polymorphism in C++
Polymorphism is one of the four fundamental Object-Oriented Programming (OOP) principles in C++. The term "polymorphism" comes from Greek words meaning "many forms." In programming, it refers to the ability to present the same interface for differing underlying forms (data types).
Key Concepts in Polymorphism
- Compile-time Polymorphism - Resolved at compile time (static binding)
- Runtime Polymorphism - Resolved at runtime (dynamic binding)
- Virtual Functions - Functions that can be overridden in derived classes
- Function Overloading - Multiple functions with the same name but different parameters
- Operator Overloading - Custom behavior for operators with user-defined types
Types of Polymorphism in C++
C++ supports two main types of polymorphism:
Compile-time Polymorphism
Also known as static polymorphism or early binding. The compiler determines which function to call at compile time. Implemented through:
- Function overloading
- Operator overloading
- Templates
Runtime Polymorphism
Also known as dynamic polymorphism or late binding. The decision about which function to call is made at runtime. Implemented through:
- Virtual functions
- Function overriding
- Virtual inheritance
Compile-time Polymorphism
Function Overloading
Function overloading allows multiple functions with the same name but different parameters (different number, types, or order of parameters). The compiler selects the appropriate function to call based on the arguments provided.
1#include <iostream>2#include <string>3using namespace std;45class MathOperations {6public:7 // Function overloading - same name, different parameter types8 int add(int a, int b) {9 cout << "Adding two integers" << endl;10 return a + b;11 }1213 double add(double a, double b) {14 cout << "Adding two doubles" << endl;15 return a + b;16 }1718 // Different number of parameters19 int add(int a, int b, int c) {20 cout << "Adding three integers" << endl;21 return a + b + c;22 }2324 // Different parameter order25 string add(string s, int n) {26 cout << "Adding int to string" << endl;27 return s + to_string(n);28 }2930 string add(int n, string s) {31 cout << "Adding string to int" << endl;32 return to_string(n) + s;33 }34};3536int main() {37 MathOperations math;3839 // Each call invokes a different version of add()40 cout << math.add(5, 10) << endl; // Calls int add(int, int)41 cout << math.add(3.5, 7.25) << endl; // Calls double add(double, double)42 cout << math.add(1, 2, 3) << endl; // Calls int add(int, int, int)43 cout << math.add("Value: ", 42) << endl; // Calls string add(string, int)44 cout << math.add(42, " is the answer") << endl; // Calls string add(int, string)4546 return 0;47}
Function Overloading Rules
Functions cannot be overloaded based solely on:
- Return type (must have different parameters)
- Default parameters (ambiguous calls can occur)
- Type qualifiers like
const
orvolatile
alone (except for member functions)
Operator Overloading
Operator overloading allows operators (+, -, *, etc.) to work with user-defined types. It enables more intuitive syntax when working with custom classes.
1#include <iostream>2using namespace std;34class Complex {5private:6 double real;7 double imag;89public:10 // Constructor11 Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}1213 // Overload + operator14 Complex operator+(const Complex& other) const {15 return Complex(real + other.real, imag + other.imag);16 }1718 // Overload - operator19 Complex operator-(const Complex& other) const {20 return Complex(real - other.real, imag - other.imag);21 }2223 // Overload * operator24 Complex operator*(const Complex& other) const {25 return Complex(26 real * other.real - imag * other.imag,27 real * other.imag + imag * other.real28 );29 }3031 // Overload == operator32 bool operator==(const Complex& other) const {33 return (real == other.real) && (imag == other.imag);34 }3536 // Overload != operator37 bool operator!=(const Complex& other) const {38 return !(*this == other);39 }4041 // Overload unary - operator42 Complex operator-() const {43 return Complex(-real, -imag);44 }4546 // Display the complex number47 void display() const {48 cout << real;49 if (imag >= 0) {50 cout << " + " << imag << "i";51 } else {52 cout << " - " << -imag << "i";53 }54 cout << endl;55 }5657 // Friend function for output stream operator overloading58 friend ostream& operator<<(ostream& os, const Complex& c);59};6061// Overload << operator as a friend function62ostream& operator<<(ostream& os, const Complex& c) {63 os << c.real;64 if (c.imag >= 0) {65 os << " + " << c.imag << "i";66 } else {67 os << " - " << -c.imag << "i";68 }69 return os;70}7172int main() {73 Complex c1(3, 4);74 Complex c2(1, -2);7576 cout << "c1 = " << c1 << endl;77 cout << "c2 = " << c2 << endl;7879 // Using overloaded operators80 Complex sum = c1 + c2;81 Complex diff = c1 - c2;82 Complex prod = c1 * c2;8384 cout << "c1 + c2 = " << sum << endl;85 cout << "c1 - c2 = " << diff << endl;86 cout << "c1 * c2 = " << prod << endl;8788 // Using comparison operators89 cout << "c1 == c2: " << (c1 == c2) << endl;90 cout << "c1 != c2: " << (c1 != c2) << endl;9192 // Using unary operator93 Complex neg = -c1;94 cout << "-c1 = " << neg << endl;9596 return 0;97}
Operator Overloading Guidelines:
- Keep the operator's original meaning (e.g., + should perform addition-like operations)
- Use member functions for operators that modify the left operand (e.g., +=, -=)
- Use non-member functions for operators where the left operand might be converted
- Always consider overloading related operators together (e.g., if you overload ==, also overload !=)
Runtime Polymorphism
Virtual Functions
Virtual functions enable runtime polymorphism in C++. A virtual function is a member function in a base class that is declared using the virtual
keyword and can be overridden in derived classes. When you call a virtual function through a base class pointer or reference, the program determines which version of the function to call at runtime.
1#include <iostream>2#include <string>3using namespace std;45class Shape {6public:7 // Virtual function8 virtual void draw() const {9 cout << "Drawing a generic shape" << endl;10 }1112 // Another virtual function13 virtual double area() const {14 cout << "Calculating generic shape area" << endl;15 return 0;16 }1718 // Virtual destructor19 virtual ~Shape() {20 cout << "Shape destructor" << endl;21 }22};2324class Circle : public Shape {25private:26 double radius;2728public:29 Circle(double r) : radius(r) {}3031 // Override virtual function32 void draw() const override {33 cout << "Drawing a circle with radius " << radius << endl;34 }3536 // Override virtual function37 double area() const override {38 cout << "Calculating circle area" << endl;39 return 3.14159 * radius * radius;40 }4142 ~Circle() override {43 cout << "Circle destructor" << endl;44 }45};4647class Rectangle : public Shape {48private:49 double width;50 double height;5152public:53 Rectangle(double w, double h) : width(w), height(h) {}5455 // Override virtual function56 void draw() const override {57 cout << "Drawing a rectangle with width " << width << " and height " << height << endl;58 }5960 // Override virtual function61 double area() const override {62 cout << "Calculating rectangle area" << endl;63 return width * height;64 }6566 ~Rectangle() override {67 cout << "Rectangle destructor" << endl;68 }69};7071int main() {72 // Create objects73 Shape* shapes[3];74 shapes[0] = new Shape();75 shapes[1] = new Circle(5.0);76 shapes[2] = new Rectangle(4.0, 6.0);7778 // Polymorphic behavior79 for (int i = 0; i < 3; i++) {80 shapes[i]->draw(); // Calls the appropriate draw() method81 cout << "Area: " << shapes[i]->area() << endl;82 cout << "------------------------" << endl;83 }8485 // Clean up (calls appropriate destructors)86 for (int i = 0; i < 3; i++) {87 delete shapes[i];88 }8990 return 0;91}
The override
Keyword
The override
keyword (introduced in C++11) helps catch errors by explicitly stating that a function is intended to override a virtual function in the base class. If the function signature doesn't match the base class version, the compiler will generate an error.
How Virtual Functions Work
Virtual functions are implemented using a mechanism called the virtual function table (vtable):
- Each class with virtual functions has a vtable, which is a table of function pointers
- Each object of such a class contains a hidden pointer (vptr) to the class's vtable
- When a virtual function is called through a base class pointer, the program looks up the correct function in the vtable
- This dynamic dispatch mechanism enables runtime polymorphism
Important:
Always make destructors virtual in base classes that are meant to be inherited from. This ensures that the proper destructor is called when an object is deleted through a base class pointer, preventing memory leaks.
Pure Virtual Functions and Abstract Classes
A pure virtual function is a virtual function with no implementation in the base class, declared by appending = 0
to the function declaration. A class with at least one pure virtual function is called an abstract class and cannot be instantiated.
1#include <iostream>2using namespace std;34// Abstract class5class AbstractShape {6public:7 // Pure virtual function8 virtual void draw() const = 0;910 // Pure virtual function11 virtual double area() const = 0;1213 // Regular virtual function with implementation14 virtual void printInfo() const {15 cout << "This is a shape with area: " << area() << endl;16 }1718 // Virtual destructor19 virtual ~AbstractShape() {20 cout << "AbstractShape destructor" << endl;21 }22};2324class Circle : public AbstractShape {25private:26 double radius;2728public:29 Circle(double r) : radius(r) {}3031 // Implementation of pure virtual function32 void draw() const override {33 cout << "Drawing a circle with radius " << radius << endl;34 }3536 // Implementation of pure virtual function37 double area() const override {38 return 3.14159 * radius * radius;39 }4041 ~Circle() override {42 cout << "Circle destructor" << endl;43 }44};4546class Rectangle : public AbstractShape {47private:48 double width;49 double height;5051public:52 Rectangle(double w, double h) : width(w), height(h) {}5354 // Implementation of pure virtual function55 void draw() const override {56 cout << "Drawing a rectangle with width " << width << " and height " << height << endl;57 }5859 // Implementation of pure virtual function60 double area() const override {61 return width * height;62 }6364 // Override regular virtual function65 void printInfo() const override {66 cout << "This is a rectangle with width " << width << " and height " << height << endl;67 AbstractShape::printInfo(); // Call base class version68 }6970 ~Rectangle() override {71 cout << "Rectangle destructor" << endl;72 }73};7475int main() {76 // AbstractShape shape; // Error: Cannot instantiate abstract class7778 AbstractShape* shapes[2];79 shapes[0] = new Circle(5.0);80 shapes[1] = new Rectangle(4.0, 6.0);8182 for (int i = 0; i < 2; i++) {83 shapes[i]->draw();84 shapes[i]->printInfo();85 cout << "------------------------" << endl;86 }8788 // Clean up89 for (int i = 0; i < 2; i++) {90 delete shapes[i];91 }9293 return 0;94}
Abstract Classes as Interfaces
In C++, abstract classes with only pure virtual functions serve a similar purpose as interfaces in other languages like Java. They define a contract that derived classes must fulfill by implementing all the pure virtual functions.
Best Practices for Polymorphism
Do
- Use
virtual
for functions meant to be overridden - Use
override
when overriding virtual functions - Make destructors virtual in base classes
- Use abstract classes to define interfaces
- Use dynamic binding for runtime flexibility
- Consider performance implications of virtual functions
Don't
- Make all functions virtual (performance overhead)
- Call virtual functions in constructors or destructors
- Use dynamic casting excessively
- Forget to implement all pure virtual functions
- Create deep inheritance hierarchies
- Confuse overloading (compile-time) with overriding (runtime)
Advanced Polymorphism Concepts
Virtual Inheritance
Virtual inheritance solves the "diamond problem" in multiple inheritance by ensuring that a derived class has only one instance of a common base class.
1#include <iostream>2using namespace std;34// Base class5class Animal {6protected:7 string name;89public:10 Animal(const string& n) : name(n) {11 cout << "Animal constructor" << endl;12 }1314 virtual void eat() const {15 cout << name << " is eating" << endl;16 }1718 virtual ~Animal() {19 cout << "Animal destructor" << endl;20 }21};2223// Derived class with virtual inheritance24class Mammal : virtual public Animal {25public:26 Mammal(const string& n) : Animal(n) {27 cout << "Mammal constructor" << endl;28 }2930 void breathe() const {31 cout << name << " is breathing with lungs" << endl;32 }3334 ~Mammal() override {35 cout << "Mammal destructor" << endl;36 }37};3839// Another derived class with virtual inheritance40class Bird : virtual public Animal {41public:42 Bird(const string& n) : Animal(n) {43 cout << "Bird constructor" << endl;44 }4546 void fly() const {47 cout << name << " is flying" << endl;48 }4950 ~Bird() override {51 cout << "Bird destructor" << endl;52 }53};5455// Class derived from both Mammal and Bird56class Bat : public Mammal, public Bird {57public:58 // Must explicitly call Animal constructor59 Bat(const string& n) : Animal(n), Mammal(n), Bird(n) {60 cout << "Bat constructor" << endl;61 }6263 // Override eat from Animal64 void eat() const override {65 cout << name << " is eating fruits" << endl;66 }6768 ~Bat() override {69 cout << "Bat destructor" << endl;70 }71};7273int main() {74 Bat bat("Bruce");7576 // Without virtual inheritance, these would be ambiguous77 bat.eat(); // From Animal78 bat.breathe(); // From Mammal79 bat.fly(); // From Bird8081 // Access Animal's name through either path - only one copy exists82 cout << "Name: " << bat.name << endl;8384 return 0;85}
Summary
Polymorphism is a powerful concept in C++ that allows for flexible and extensible code. By understanding both compile-time and runtime polymorphism, you can create more modular and reusable code structures.
- Compile-time polymorphism is achieved through function overloading and operator overloading
- Runtime polymorphism is achieved through virtual functions and inheritance
- Virtual functions enable dynamic method dispatch based on the actual type of an object
- Pure virtual functions define interfaces that derived classes must implement
- Abstract classes provide a way to define common behavior while requiring specific implementations
- Virtual inheritance solves the diamond problem in multiple inheritance
Mastering polymorphism is essential for effective object-oriented design in C++. It enables you to create frameworks that can be extended without modifying existing code, following the open-closed principle of software design.
Related Tutorials
Learn about classes and objects in C++.
Learn moreUnderstand inheritance and code reuse in C++.
Learn moreExplore data hiding and access control in C++.
Learn more