Progress13 of 20 topics

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.

cpp
1#include <iostream>
2#include <string>
3using namespace std;
4
5class MathOperations {
6public:
7 // Function overloading - same name, different parameter types
8 int add(int a, int b) {
9 cout << "Adding two integers" << endl;
10 return a + b;
11 }
12
13 double add(double a, double b) {
14 cout << "Adding two doubles" << endl;
15 return a + b;
16 }
17
18 // Different number of parameters
19 int add(int a, int b, int c) {
20 cout << "Adding three integers" << endl;
21 return a + b + c;
22 }
23
24 // Different parameter order
25 string add(string s, int n) {
26 cout << "Adding int to string" << endl;
27 return s + to_string(n);
28 }
29
30 string add(int n, string s) {
31 cout << "Adding string to int" << endl;
32 return to_string(n) + s;
33 }
34};
35
36int main() {
37 MathOperations math;
38
39 // 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)
45
46 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 or volatile 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.

cpp
1#include <iostream>
2using namespace std;
3
4class Complex {
5private:
6 double real;
7 double imag;
8
9public:
10 // Constructor
11 Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}
12
13 // Overload + operator
14 Complex operator+(const Complex& other) const {
15 return Complex(real + other.real, imag + other.imag);
16 }
17
18 // Overload - operator
19 Complex operator-(const Complex& other) const {
20 return Complex(real - other.real, imag - other.imag);
21 }
22
23 // Overload * operator
24 Complex operator*(const Complex& other) const {
25 return Complex(
26 real * other.real - imag * other.imag,
27 real * other.imag + imag * other.real
28 );
29 }
30
31 // Overload == operator
32 bool operator==(const Complex& other) const {
33 return (real == other.real) && (imag == other.imag);
34 }
35
36 // Overload != operator
37 bool operator!=(const Complex& other) const {
38 return !(*this == other);
39 }
40
41 // Overload unary - operator
42 Complex operator-() const {
43 return Complex(-real, -imag);
44 }
45
46 // Display the complex number
47 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 }
56
57 // Friend function for output stream operator overloading
58 friend ostream& operator<<(ostream& os, const Complex& c);
59};
60
61// Overload << operator as a friend function
62ostream& 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}
71
72int main() {
73 Complex c1(3, 4);
74 Complex c2(1, -2);
75
76 cout << "c1 = " << c1 << endl;
77 cout << "c2 = " << c2 << endl;
78
79 // Using overloaded operators
80 Complex sum = c1 + c2;
81 Complex diff = c1 - c2;
82 Complex prod = c1 * c2;
83
84 cout << "c1 + c2 = " << sum << endl;
85 cout << "c1 - c2 = " << diff << endl;
86 cout << "c1 * c2 = " << prod << endl;
87
88 // Using comparison operators
89 cout << "c1 == c2: " << (c1 == c2) << endl;
90 cout << "c1 != c2: " << (c1 != c2) << endl;
91
92 // Using unary operator
93 Complex neg = -c1;
94 cout << "-c1 = " << neg << endl;
95
96 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.

cpp
1#include <iostream>
2#include <string>
3using namespace std;
4
5class Shape {
6public:
7 // Virtual function
8 virtual void draw() const {
9 cout << "Drawing a generic shape" << endl;
10 }
11
12 // Another virtual function
13 virtual double area() const {
14 cout << "Calculating generic shape area" << endl;
15 return 0;
16 }
17
18 // Virtual destructor
19 virtual ~Shape() {
20 cout << "Shape destructor" << endl;
21 }
22};
23
24class Circle : public Shape {
25private:
26 double radius;
27
28public:
29 Circle(double r) : radius(r) {}
30
31 // Override virtual function
32 void draw() const override {
33 cout << "Drawing a circle with radius " << radius << endl;
34 }
35
36 // Override virtual function
37 double area() const override {
38 cout << "Calculating circle area" << endl;
39 return 3.14159 * radius * radius;
40 }
41
42 ~Circle() override {
43 cout << "Circle destructor" << endl;
44 }
45};
46
47class Rectangle : public Shape {
48private:
49 double width;
50 double height;
51
52public:
53 Rectangle(double w, double h) : width(w), height(h) {}
54
55 // Override virtual function
56 void draw() const override {
57 cout << "Drawing a rectangle with width " << width << " and height " << height << endl;
58 }
59
60 // Override virtual function
61 double area() const override {
62 cout << "Calculating rectangle area" << endl;
63 return width * height;
64 }
65
66 ~Rectangle() override {
67 cout << "Rectangle destructor" << endl;
68 }
69};
70
71int main() {
72 // Create objects
73 Shape* shapes[3];
74 shapes[0] = new Shape();
75 shapes[1] = new Circle(5.0);
76 shapes[2] = new Rectangle(4.0, 6.0);
77
78 // Polymorphic behavior
79 for (int i = 0; i < 3; i++) {
80 shapes[i]->draw(); // Calls the appropriate draw() method
81 cout << "Area: " << shapes[i]->area() << endl;
82 cout << "------------------------" << endl;
83 }
84
85 // Clean up (calls appropriate destructors)
86 for (int i = 0; i < 3; i++) {
87 delete shapes[i];
88 }
89
90 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):

  1. Each class with virtual functions has a vtable, which is a table of function pointers
  2. Each object of such a class contains a hidden pointer (vptr) to the class's vtable
  3. When a virtual function is called through a base class pointer, the program looks up the correct function in the vtable
  4. 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.

cpp
1#include <iostream>
2using namespace std;
3
4// Abstract class
5class AbstractShape {
6public:
7 // Pure virtual function
8 virtual void draw() const = 0;
9
10 // Pure virtual function
11 virtual double area() const = 0;
12
13 // Regular virtual function with implementation
14 virtual void printInfo() const {
15 cout << "This is a shape with area: " << area() << endl;
16 }
17
18 // Virtual destructor
19 virtual ~AbstractShape() {
20 cout << "AbstractShape destructor" << endl;
21 }
22};
23
24class Circle : public AbstractShape {
25private:
26 double radius;
27
28public:
29 Circle(double r) : radius(r) {}
30
31 // Implementation of pure virtual function
32 void draw() const override {
33 cout << "Drawing a circle with radius " << radius << endl;
34 }
35
36 // Implementation of pure virtual function
37 double area() const override {
38 return 3.14159 * radius * radius;
39 }
40
41 ~Circle() override {
42 cout << "Circle destructor" << endl;
43 }
44};
45
46class Rectangle : public AbstractShape {
47private:
48 double width;
49 double height;
50
51public:
52 Rectangle(double w, double h) : width(w), height(h) {}
53
54 // Implementation of pure virtual function
55 void draw() const override {
56 cout << "Drawing a rectangle with width " << width << " and height " << height << endl;
57 }
58
59 // Implementation of pure virtual function
60 double area() const override {
61 return width * height;
62 }
63
64 // Override regular virtual function
65 void printInfo() const override {
66 cout << "This is a rectangle with width " << width << " and height " << height << endl;
67 AbstractShape::printInfo(); // Call base class version
68 }
69
70 ~Rectangle() override {
71 cout << "Rectangle destructor" << endl;
72 }
73};
74
75int main() {
76 // AbstractShape shape; // Error: Cannot instantiate abstract class
77
78 AbstractShape* shapes[2];
79 shapes[0] = new Circle(5.0);
80 shapes[1] = new Rectangle(4.0, 6.0);
81
82 for (int i = 0; i < 2; i++) {
83 shapes[i]->draw();
84 shapes[i]->printInfo();
85 cout << "------------------------" << endl;
86 }
87
88 // Clean up
89 for (int i = 0; i < 2; i++) {
90 delete shapes[i];
91 }
92
93 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.

cpp
1#include <iostream>
2using namespace std;
3
4// Base class
5class Animal {
6protected:
7 string name;
8
9public:
10 Animal(const string& n) : name(n) {
11 cout << "Animal constructor" << endl;
12 }
13
14 virtual void eat() const {
15 cout << name << " is eating" << endl;
16 }
17
18 virtual ~Animal() {
19 cout << "Animal destructor" << endl;
20 }
21};
22
23// Derived class with virtual inheritance
24class Mammal : virtual public Animal {
25public:
26 Mammal(const string& n) : Animal(n) {
27 cout << "Mammal constructor" << endl;
28 }
29
30 void breathe() const {
31 cout << name << " is breathing with lungs" << endl;
32 }
33
34 ~Mammal() override {
35 cout << "Mammal destructor" << endl;
36 }
37};
38
39// Another derived class with virtual inheritance
40class Bird : virtual public Animal {
41public:
42 Bird(const string& n) : Animal(n) {
43 cout << "Bird constructor" << endl;
44 }
45
46 void fly() const {
47 cout << name << " is flying" << endl;
48 }
49
50 ~Bird() override {
51 cout << "Bird destructor" << endl;
52 }
53};
54
55// Class derived from both Mammal and Bird
56class Bat : public Mammal, public Bird {
57public:
58 // Must explicitly call Animal constructor
59 Bat(const string& n) : Animal(n), Mammal(n), Bird(n) {
60 cout << "Bat constructor" << endl;
61 }
62
63 // Override eat from Animal
64 void eat() const override {
65 cout << name << " is eating fruits" << endl;
66 }
67
68 ~Bat() override {
69 cout << "Bat destructor" << endl;
70 }
71};
72
73int main() {
74 Bat bat("Bruce");
75
76 // Without virtual inheritance, these would be ambiguous
77 bat.eat(); // From Animal
78 bat.breathe(); // From Mammal
79 bat.fly(); // From Bird
80
81 // Access Animal's name through either path - only one copy exists
82 cout << "Name: " << bat.name << endl;
83
84 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 more

Understand inheritance and code reuse in C++.

Learn more

Explore data hiding and access control in C++.

Learn more