1 Definition
Polymorphism is indeed derived from the Greek words "poly," meaning many, and "morph," meaning form. For example, think of a function that adds numbers. With polymorphism, it could add two numbers together or concatenate two strings, depending on what you give it. This flexibility makes our code more adaptable and versatile.
In simple terms, polymorphism is like having a tool that can wear many hats, doing different jobs depending on what's needed.
Polymorphism is a fundamental concept in object-oriented programming (OOP) that allows objects of different classes to be treated as objects of a common base class type.
2 Types of Polymorphism
2.1 Compile-time Polymorphism (Static Polymorphism)(early binding):
Compile-time polymorphism, also known as static polymorphism, refers to the polymorphic behavior that is determined and resolved by the compiler at compile time. It is achieved through function overloading and operator overloading.
Function Overloading: Function overloading is a form of compile-time polymorphism where multiple functions with the same name but different parameter lists are defined within the same scope. The compiler determines which function to call based on the number and types of arguments passed to the function at compile time.
Function overloading can be achieved through the following two cases:
- The names of the functions and return types are the same but differ in the type of arguments.
- The name of the functions and return types are the same, but they differ in the number of arguments.
Example:
#include <bits/stdc++.h>
using namespace std;
class Temp
{
private:
int x = 10;
double x1 = 10.1;
public:
void add(int y)
{
cout << "Value of x + y is: " << x + y << endl;
}
// Differ in the type of argument.
void add(double d)
{
cout << "Value of x1 + d is: " << x1 + d << endl;
}
// Differ in the number of arguments.
void add(int y, int z)
{
cout << "Value of x + y + z is: " << x + y + z << endl;
}
};
int main() {
Temp t1;
t1.add(10);
t1.add(11.1);
t1.add(12,13);
return 0;
}
Operator Overloading: Operator overloading allows operators to be redefined for user-defined types, enabling the same operator to exhibit different behaviors based on the operands' types. It is also a form of compile-time polymorphism.
. :: typeid size .*
and ternary operator (?:
) are among the operators that cannot be overloaded.
Note:
New and Delete operators can be overloaded globally, or they can be overloaded for specific classes. If these operators are overloaded using the member function for a class, they are overloaded only for that specific class.
Example:
#include <iostream>
// Operator overloading
class Complex {
private:
double real;
double imag;
public:
Complex(double r, double i) : real(r), imag(i) {}
Complex operator+(const Complex& other) {
return Complex(real + other.real, imag + other.imag);
}
friend std::ostream& operator<<(std::ostream& os, const Complex& c) {
os << c.real << " + " << c.imag << "i";
return os;
}
};
int main() {
Complex c1(2.5, 3.7);
Complex c2(1.8, 2.3);
Complex sum = c1 + c2;
std::cout << "Sum of complex numbers: " << sum << std::endl;
return 0;
}
2.2 Runtime Polymorphism (Dynamic Polymorphism)(late binding):
Runtime polymorphism, also known as dynamic polymorphism, refers to the polymorphic behavior that is determined and resolved at runtime. It allows different objects to be treated as objects of a common base class type, enabling more flexible and extensible code. Runtime polymorphism is achieved in C++ through virtual functions and inheritance.
Runtime polymorphism occurs when functions are resolved at runtime rather than compile time when a call to an overridden method is resolved dynamically at runtime rather than compile time. It's also known as late binding or dynamic binding.
Function Overriding: Allows a derived class to provide a specific implementation of a function that is already defined in its base class. It occurs when a derived class has a definition for one of the member functions of the base class. That base function is said to be overridden.
Function overriding without Virtual Keyword:
#include <iostream>
class Base {
public:
void display() {
std::cout << "Base display() function" << std::endl;
}
};
class Derived : public Base {
public:
void display() {
std::cout << "Derived display() function" << std::endl;
}
};
int main() {
Derived derivedObj;
derivedObj.display(); // Calls the Derived class display() function
Base* basePtr = &derivedObj;
basePtr->display(); // Calls the Base class display() function (function hiding)
return 0;
}
// Output
Derived display() function
Base display() function
In this example, the Derived
class defines its own display()
function, which hides the display()
function of the Base class. However, when calling display()
through a base class pointer basePtr
, the function of the base class is invoked, demonstrating function hiding instead of function overriding.
Virtual Functions: Virtual functions are functions declared in a base class using the virtual keyword. They are intended to be overridden by derived classes, and their behavior is determined based on the actual type of the object at runtime. When a virtual function is called through a base class pointer or reference, the actual function to be executed is resolved dynamically at runtime.
The same example with virtual keyword:
#include <iostream>
class Base {
public:
virtual void display() {
std::cout << "Base display() function" << std::endl;
}
};
class Derived : public Base {
public:
void display() override {
std::cout << "Derived display() function" << std::endl;
}
};
int main() {
Derived derivedObj;
derivedObj.display(); // Calls the Derived class display() function
Base* basePtr = &derivedObj;
basePtr->display(); // Calls the Derived class display() function (function overriding)
return 0;
}
// Output
Derived display() function
Derived display() function
In this example, the display()
function in the Base
class is declared as virtual
, indicating that it can be overridden by derived classes. The display()
function in the Derived class is marked with the override
keyword, explicitly indicating that it overrides the virtual function from the base class.
When calling display()
through a base class pointer basePtr
, the function of the derived class is invoked, demonstrating function overriding. This behavior is achieved through dynamic polymorphism, enabled by the virtual
keyword.
Example:
#include <iostream>
// Base class with a virtual function
class Animal {
public:
virtual void makeSound() {
std::cout << "Animal makes a sound" << std::endl;
}
};
// Derived class overriding the virtual function
class Dog : public Animal {
public:
void makeSound() override {
std::cout << "Dog barks" << std::endl;
}
};
int main() {
// Base class pointer pointing to a derived class object
Animal* animal = new Dog();
// Call to virtual function resolved dynamically at runtime
animal->makeSound(); // Output: "Dog barks"
delete animal;
return 0;
}
In this example, the Animal class has a virtual function makeSound(), which is overridden in the Dog class. When makeSound() is called through a base class pointer animal pointing to a Dog object, the overridden function makeSound() in the Dog class is invoked dynamically at runtime, resulting in the output "Dog barks".
3 Abstract Classes
An abstract class is a class that cannot be instantiated. It is meant to serve as a base class for other classes. It may contain one or more pure virtual functions and can have both regular member functions and data members.
Characteristics:
- An abstract class cannot be instantiated directly; it can only be used as a base class.
- It may contain member variables, member functions with implementations, and pure virtual functions.
- Any class that contains at least one pure virtual function is considered abstract.
- Abstract classes are meant to be extended by derived classes, which provide concrete implementations for the pure virtual functions.
Syntax:
An abstract class is declared by having at least one pure virtual function, which is achieved by appending = 0
to the function declaration.
Example:
class Shape {
public:
// Pure virtual function
virtual void draw() const = 0;
// Regular member function
void print() const {
std::cout << "This is a shape." << std::endl;
}
};
Usage:
Abstract classes provide an interface for derived classes to follow. They define a common set of methods that derived classes must implement, but they may also provide default implementations for some methods.
4 Pure Virtual Functions
A pure virtual function is a virtual function that has no implementation in the base class. It is declared using the = 0
syntax and must be overridden by derived classes.
Syntax: A pure virtual function is declared by appending = 0
to the function declaration in the base class.
Example:
class Animal {
public:
// Pure virtual function
virtual void speak() const = 0;
};
Usage:
Pure virtual functions define an interface without providing a default implementation. They force derived classes to provide their own implementation, ensuring that objects of derived classes can be used polymorphically through base class pointers or references.
#include <iostream>
// Abstract class with a pure virtual function
class Shape {
public:
// Pure virtual function to calculate area
virtual double calculateArea() const = 0;
// Normal member function
void display() const {
std::cout << "This is a shape." << std::endl;
}
};
// Concrete derived class Circle
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
// Override pure virtual function to calculate area
double calculateArea() const override {
return 3.14 * radius * radius;
}
};
int main() {
// Shape is an abstract class and cannot be instantiated directly
// Shape shape; // Error: Cannot instantiate abstract class
// Create a Circle object
Circle circle(5.0);
// Call the pure virtual function through the base class pointer
Shape* shapePtr = &circle;
std::cout << "Area of circle: " << shapePtr->calculateArea() << std::endl;
return 0;
}
Feature | Compile-Time Polymorphism | Runtime Polymorphism |
---|---|---|
Mechanism | Achieved through function overloading and operator overloading | Achieved through function overriding and virtual functions |
Resolution | Resolved at compile time | Resolved at runtime |
Known as | Static polymorphism | Dynamic polymorphism |
Binding | Early binding or static binding | Late binding or dynamic binding |
Performance | Generally faster | May incur slight runtime overhead due to virtual function mechanism |
Examples | Function overloading | Function overriding, virtual functions |
Usage | Best suited for simple and fixed behaviors | Best suited for scenarios requiring flexibility and extensibility |
Code | Known at compile time | Known at runtime |
Flexibility | Limited flexibility | Greater flexibility |
Error detection | Errors detected at compile time | Errors detected at runtime |
In-Depth Virtual Functions
- The virtual table is a lookup table of functions used to resolve function calls in a dynamic/late binding manner. It is also called by other names, such as
vtable
,virtual metho table
, ordispatch table
.- In C++, virtual function resolution is sometimes called
dynamic dispatch
. - Every class that has virtual function has a corresponding virtual table. This table is a static array the compiler sets up at compile time.
- It contains one entry for each virtual function that can be called by objects of that class.
- Each entry in this table is simply a function pointer that points to the most-derived function accessible by that class.
- In C++, virtual function resolution is sometimes called
- The compiler also adds a hidden pointer that is a member of the base class, which we will call
*__vptr
.*__vptr
is set (automatically) when a class object is created so that it points to the virtual table for that class.- Unlike the this pointer, which is actually a function parameter used by the compiler to resolve self-references,
*__vptr
is a real pointer member.
Dynamic Dispatch (Late Binding)
It is a mechanism by which a call to an overridden function is resolved at runtime, rather than at compile-time. This means that the actual function that gets executed is determined based on the object's actual type, rather than its static type.
In this context, dispatching simply means determining the correct function to invoke. Generally, when you define a method within a class, the compiler stores its definition and ensures that it is executed whenever that method is called.
Consider the following example:
#include <iostream>
using namespace std;
class base
{
public:
void foo();
};
void A::foo()
{
cout << "Foo from the base" << endl;
}
Here, the compiler will create a function for foo()
and remember its address. This routine will be executed every time the compiler finds a call to foo()
on an instance of A
. Keep in mind that only one routine exists per class method, and is shared by all instances of the class. This process is known as static dispatch or early binding
. The compiler knows which routine to execute during compilation.
What do vtables
have to do with all this?
Well, there are cases where it is not possible for the compiler to know which routine to execute at compile time. This is the case, for instance, when we declare virtual functions:
#include <iostream>
using namespace std;
class Base
{
public:
virtual void bar();
virtual void jar();
};
void Base::bar()
{
cout << "Base's bar" << endl;
}
void Base::jar()
{
cout << "Base's jar" << endl;
}
The thing about virtual functions is that they can be overridden by subclasses:
class Derived: public Base
{
public:
void bar() override;
};
void Derived::bar()
{
cout << Derived's bar << endl;
}
Now consider the following call to bar()
:
Base* base = new Derived();
base->bar();
If we use static dispatch as above, the call base->bar()
would execute Base::bar()
, since (from the point of view of the compiler) base
points to an object of type Base
. This would be horribly wrong, off course, because Base
actually points to an object of type Derived
and Derived::bar()
should be called instead.
Hopefully you can see the problem by now: given that virtual functions can be redefined in subclasses, calls via pointers (or references) to a base type can not be dispatched at compile time. The compiler has to find the right function definition (i.e. the most specific one) at runtime. This process is called dynamic dispatch or late method binding.
How do the Compiler dynamic dispatch?
For every class that contains virtual functions, the compiler constructs a virtual table (vtable
). The vtable
contains an entry for each virtual function accessible by the class and stores a pointer to its definition. Only the most specific function definition callable by the class is stored in the vtable
. Entries in the vtable
can point to either functions declared in the class itself (e.g., Derived::bar()
), or virtual functions inherited from a base class (e.g. Derived::jar()
).
The vtable
of class Base
has two entries, one for each of the two virtual functions declared in Base's scope: bar()
and jar()
. Additionally, the vtable of Base
points to the local definition of functions, since they are the most specific from Base
's point of view.
The Derived
class's vtable
has also two entries, in which the entry for bar()
points to its own implementation, given that it is more specific than Base::bar()
. Since Derived
does not override jar()
, its entry in the vtable
points to Base
's definition (the most specific definition).
vtable
exists at the class level, meaning there exists a singlevtable
per class, and is shared by all instances.
Vpointer
:
When the compiler sees base->bar()
in the example above, it will lookup Base
's vtable
for bar
's entry and follow the corresponding function pointer. We would still be calling Base::bar()
not Derived::bar()
.
Every time the compiler creates a vtable
for a class, it adds an extra argument to it: a pointer to the corresponding virtual table, called the Vpointer
.
vpointer
is just another class member added by the compiler and increase the size of every that has avtable
bysizeof(vpointer)
.
When a call to a virtual function on an object is performed, the vpointer
of the object is used to find the corresponding vtable
of the class. Next, the function name is used as index to the vtable
to find the correct (most specific) routine to be executed.
Base* base = new Derived();
base->bar();
The compiler sees base->bar()
. It fetches the vpointer
from the Base
. Using this vpointer
, it accesses the Derived
's vtable
and fetches pointer for bar()
, which points to Derived::bar()
.
class Base
{
public:
VirtualTable* __vptr;
virtual void function1() {};
virtual void function2() {};
};
class D1: public Base
{
public:
void function1() override {};
};
class D2: public Base
{
public:
void function2() override {};
};
When a class object is created, *__vptr
is set to point to the virtual table for that class. For example, when an object of type Base
is created, *__vptr
is set to point to the virtual table for Base
. When objects of type D1
or D2
are constructed, *__vptr
is set to point to the virtual table for D1
or D2
respectively.
Because there are only two virtual functions here, each virtual table will have two entries (one for function1() and one for function2()). Remember that when these virtual tables are filled out, each entry is filled out with the most-derived function an object of that class type can call.
The virtual table for Base objects is simple. An object of type Base
can only access the members of Base
. Base
has no access to D1
or D2
functions. Consequently, the entry for function1 points to Base::function1()
and the entry for function2 points to Base::function2()
.
int main()
{
D1 d1 {};
}
Because d1
is a D1
object, d1
has its *__vptr
set to the D1
virtual table.
Now, let's set a base pointer to D1
:
int main()
{
D1 d1 {};
Base* dPtr = &d1;
return 0;
}
Note that because dPtr
is a base pointer, it only points to the Base
portion of d1
. However, also note that *__vptr
is in the Base
portion of the class, so dPtr
has access to this pointer. Finally, note that dPtr->__vptr
points to the D1
virtual table. Consequently, even though dPtr
is of type Base*
, it still has access to D1’s virtual table
(through __vptr
).
int main()
{
D1 d1 {};
Base* dPtr = &d1;
dPtr->function1();
return 0;
}
First, the program recognizes that function1()
is a virtual function. Second, the program uses dPtr->__vptr
to get to D1’s virtual table
. Third, it looks up which version of function1()
to call in D1’s virtual table
. This has been set to D1::function1()
. Therefore, dPtr->function1()
resolves to D1::function1()
.
Now in the case when Base object is pointing to the Base object instead of a D1
object.
int main()
{
Base b {};
Base* bPtr = &b;
bPtr->function1();
return 0;
}
In this case, when b
is created, b.__vptr
points to Base
’s virtual table, not D1
’s virtual table. Since bPtr
is pointing to b
, bPtr->__vptr
points to Base
’s virtual table as well. Base’s virtual table entry for function1()
points to Base::function1()
. Thus, bPtr->function1()
resolves to Base::function1()
, which is the most-derived version of function1()
that a Base object should be able to call.
Note:
Calling a virtual function is slower than calling a non-virtual function for a couple of reasons:
- First, we have to use the
*__vptr
to get to the appropriate virtual table. - Second, we have to index the virtual table to find the correct function to call. Only then can we call the function. As a result, we have to do 3 operations to find the function to call, as opposed to 2 operations for a normal indirect function call, or one operation for a direct function call. However, with modern computers, this added time is usually fairly insignificant.
Also as a reminder, any class that uses virtual functions has a *__vptr
, and thus each object of that class will be bigger by one pointer. Virtual functions are powerful, but they do have a performance cost.
The Role of Virtual Functions:
In languages like C++, a function is made “virtual” to indicate that it can be overridden by derived classes and that dynamic dispatch should be used when invoking this function on a pointer or reference to the base type.
How Does It Work?
When a function is declared as virtual in a base class:
- A table called the **vtable** (virtual table) is created for that class
- Every class that inherits from this base class and overrides this virtual function gets its own vtable
- The vtable contains addresses of the actual functions that should be called for objects of a particular type
When a virtual function is called on an object:
- The system looks up the object’s vtable based on its actual type
- It finds the address of the function to execute
- It calls the function at that address
vtable (Virtual Table):
A vtable (Virtual Table) is an internal mechanism used by the compiler to support runtime polymorphism for virtual functions. It is essentially a lookup table of pointers to the virtual functions of a class.
- Each class that has virtual functions gets its own vtable.
- The vtable holds the addresses of the class's virtual functions.
- If a class overrides a virtual function from its base class, the vtable will hold a pointer to the derived class's version of the function.
How it works:
- At runtime, when a virtual function is called, the vtable is used to look up the correct function to execute.
- The lookup is based on the actual type of the object, not the type of the pointer or reference to the object.
vptr (Virtual Pointer):
The vptr (Virtual Pointer) is a hidden pointer maintained in each object that points to the vtable of its class. The vptr is set automatically when an object of a class with virtual functions is created.
- Every object of a class with virtual functions has a vptr.
- The vptr points to the vtable for that object’s class.
- When a virtual function is called on an object, the vptr is used to access the vtable to find the correct function to call.
How It All Works Together:
- vptr points to the vtable of the object’s actual class.
- When a virtual function is called, the program accesses the vtable via the object’s vptr.
- The correct function pointer is retrieved from the vtable and called.
Virtual Destructors
In C++, virtual destructors are used to ensure that when an object of a derived class is deleted through a pointer to a base class, the destructor of the derived class is called properly. This is crucial in situations involving inheritance, especially when dynamically allocated memory or resources are managed in the derived class.
Here’s why virtual destructors are important:
- Without a virtual destructor, if you delete an object through a base class pointer, only the base class destructor will be called, potentially leading to resource leaks or incomplete cleanup.
- With a virtual destructor, deleting an object through a base class pointer ensures that both the base class and derived class destructors are invoked, cleaning up resources properly.
Example:
#include <iostream>
class Base
{
public:
~Base()
{
std::cout << "Destroying base" << std::endl;
}
};
class Derived : public Base
{
public:
Derived(int number)
{
some_resource_ = new int(number);
}
~Derived()
{
std::cout << "Destroying derived" << std::endl;
delete some_resource_;
}
private:
int* some_resource_;
};
int main()
{
Base* p = new Derived(5);
delete p;
}
This will output:
Destroying base
However, making Base
's destructor virtual will result in expected behavior, calling destructors of both the functions:
class Base {
public:
virtual ~Base() {
std::cout << "Base Destructor\n";
}
};
class Derived : public Base {
public:
~Derived() {
std::cout << "Derived Destructor\n";
}
};
int main() {
Base* obj = new Derived();
delete obj; // Both Derived and Base destructors are called
return 0;
}
In this example, the Base
class has a virtual destructor. When delete obj;
is called, both the Derived
and Base
destructors are invoked. Without the virtual destructor in Base
, only the Base
destructor would be called, possibly leaving resources in Derived
unmanaged.