The Concept of Polymorphism
Polymorphism describes the ability of different objects to respond uniquely to the same method call. When distinct objects perform an identical action, the outcome varies according to their concrete types. For instance, invoking a vocalize method returns a bark for a Dog instance but a meow for a Cat instance.
Core Mechanisms: Virtual Functions and Overriding
Declaring Virtual Functions
A member function prefixed with the virtual keyword becomes a virtual function, enabling runtime dispatch.
class Animal {
public:
virtual void vocalize() {
std::cout << "Generic animal sound" << std::endl;
}
};
Overriding in Derived Classes
A derived class overrides a base virtual function by providing a method with an identical signature—same name, parameters, and return type. The virtual qualifier is optional in the derived class but recommended for clarity.
class Dog : public Animal {
public:
void vocalize() override {
std::cout << "Bark" << std::endl;
}
};
class Cat : public Animal {
public:
void vocalize() override {
std::cout << "Meow" << std::endl;
}
};
Overriding follows two special rules:
- Covariant return types: An override may return a pointer or reference to a derived type when the base virtual function returns a pointer or reference to a base type.
- Destructor overriding: Destructors are treated as a single virtual function by the compiler. Declaring a base class destructor
virtualguarantees the correct chain of destruction when deleting objects through a base pointer. In the example below, deletingp2correctly invokes~Studentfollowed by~Person.
class Person {
public:
virtual ~Person() { std::cout << "~Person" << std::endl; }
};
class Student : public Person {
public:
virtual ~Student() { std::cout << "~Student" << std::endl; }
};
int main() {
Person* p1 = new Person();
Person* p2 = new Student();
delete p1;
delete p2;
return 0;
}
Conditions for Polymorphic Behavior
Polymorphism arises from inheritance when:
- A virtual function is invoked through a base class pointer or reference.
- The function is declared
virtualin the base class and overridden in the derived class.
Compiler Assists: override and final
C++11 supplies two contextual keywords to safeguard overriding.
finalprevents further overriding in deeper inheritance levels.overrideexplicitly signals an intended override and triggers a compilation error if no matching base virtual function exists.
class Mammal {
public:
virtual void breathe() final { /* ... */ }
};
class Bat : public Mammal {
public:
// Compilation error: cannot override a final function
// virtual void breathe() override {}
};
class Reptile {
public:
virtual void move() {}
};
class Snake : public Reptile {
public:
// Compilation error without a base virtual to override
// virtual void moves() override {}
};
Comparison: Overloading, Overriding, and Name Hiding
- Overloading: Functions in the same scope share an identifier but differ in parameter types or counts.
- Overriding (Covering): Virtual functions in base and derived scopes possess identical signatures, subverting the base implementation at runtime.
- Name Hiding (Redefinition): A derived class defines a non-virtual member sharing the same name as a base member, obscuring base versions regardless of signatures.
Abstract Classes and Pure Virtual Functions
An abstract class contains at least one pure virtual function, declared by appending = 0 to the declarasion. Such classes cannot be instantiated directly; derived types must supply implementations for all pure virtual functions to become concrete.
class Shape {
public:
virtual double area() const = 0;
};
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override { return 3.14159 * radius * radius; }
};
Abstract classes define a mandatory interface, guaranteeing that every derived concrete type fulfills the required behaviors.
How Polymorphism Works Internally
The Virtual Function Table
Each class containing or inheriting virtual functions is associated with a virtual table (vtable), an array of function pointers. Every object of such a class holds a hidden pointer, __vfptr, referencing the vtable corresponding to its dynamic type.
class Base {
public:
virtual void func1() { std::cout << "Base::func1" << std::endl; }
virtual void func2() { std::cout << "Base::func2" << std::endl; }
void func3() {} // non-virtual, not in vtable
private:
int _value = 1;
};
class Derived : public Base {
public:
void func1() override { std::cout << "Derived::func1" << std::endl; }
virtual void extra() {}
private:
int _data = 2;
};
In this setup, Derived inherits Base's vtable layout: the pointer to func1 is replaced with Derived::func1 while func2 remains unchanged. Newly introduced virtual functions are appended at the end of the derived vtable. These vtables are typically stored in a read-only memory segment.
Runtime Dispatch
When a virtual call occurs via a base reference or pointer, the generated machine code follows the __vfptr to the object’s vtable and invokes the function at the appropriate index. This resolution happens at runtime rather than compile time, enabling polymorphic behavior.
Static vs. Dynamic Binding
- Static binding: The target function is determined during compilation. Overloaded functions and non-virtual calls exemplify this.
- Dynamic binding: The concrete function is selected at runtime based on the object’s vtable, forming the basis of runtime polymorphism.