Virtual Functions, vtables & Polymorphism
Dive into runtime polymorphism: how virtual dispatch works under the hood, when to use override and final, abstract classes, and RTTI.
Runtime Polymorphism
Polymorphism means "many forms" — the same interface behaves differently depending on the actual type of the object. In C++, runtime polymorphism is achieved through virtual functions. When you call a virtual function through a base pointer or reference, the call is dispatched to the actual derived class's implementation at runtime.
This is the mechanism that makes OOP powerful: you can write code that operates on a base class interface, and it automatically works correctly with any derived class — even ones written after your code.
How vtables Work
When a class has virtual functions, the compiler creates a vtable (virtual function table) — an array of function pointers. Each object of that class contains a hidden vptr (pointer to the vtable). When a virtual function is called, the program follows the vptr to the vtable, looks up the function pointer, and calls through it. This indirection is why virtual calls have a small overhead compared to direct calls.
#include <iostream>
#include <memory>
#include <vector>
class Shape {
public:
virtual ~Shape() = default;
// Pure virtual — no default implementation
virtual double area() const = 0;
// Virtual with default implementation
virtual std::string name() const { return "Shape"; }
};
class Circle : public Shape {
double radius_;
public:
explicit Circle(double r) : radius_{r} {}
double area() const override {
return 3.14159265 * radius_ * radius_;
}
std::string name() const override { return "Circle"; }
};
class Rectangle : public Shape {
double w_, h_;
public:
Rectangle(double w, double h) : w_{w}, h_{h} {}
double area() const override { return w_ * h_; }
std::string name() const override { return "Rectangle"; }
};
// Works with ANY Shape — past, present, or future
void print_info(const Shape& s) {
std::cout << s.name() << ": area = " << s.area() << '\n';
}
int main() {
Circle c{5.0};
Rectangle r{3.0, 4.0};
print_info(c); // Circle: area = 78.5398
print_info(r); // Rectangle: area = 12
// Polymorphic container
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(2.0));
shapes.push_back(std::make_unique<Rectangle>(6.0, 3.0));
for (const auto& s : shapes) {
print_info(*s); // virtual dispatch at runtime
}
return 0;
}Virtual Destructors Are Mandatory
If a class has any virtual function, its destructor must be virtual. Without it, deleting a derived object through a base pointer causes undefined behavior — the derived destructor never runs, leaking resources.
#include <iostream>
#include <memory>
class Base {
public:
// Without virtual destructor: UNDEFINED BEHAVIOR on delete
// virtual ~Base() = default; // ALWAYS do this!
~Base() { std::cout << "~Base\n"; }
};
class Derived : public Base {
int* data_;
public:
Derived() : data_{new int[1000]} {
std::cout << "Derived: allocated\n";
}
~Derived() {
delete[] data_;
std::cout << "~Derived: freed\n";
}
};
int main() {
Base* ptr = new Derived{};
delete ptr; // Only ~Base runs! ~Derived is NOT called!
// Result: memory leak (data_ never freed)
// Fix: make ~Base() virtual
return 0;
}override and final Keywords
The override keyword tells the compiler you intend to override a base class virtual function. If the signature doesn't match (a common typo), the compiler catches it. The final keyword prevents further overriding of a function or prevents inheritance from a class.
#include <iostream>
class Base {
public:
virtual ~Base() = default;
virtual void process(int x) const { std::cout << "Base: " << x << '\n'; }
virtual void execute() { std::cout << "Base::execute\n"; }
};
class Middle : public Base {
public:
// override: compiler checks signature matches Base::process
void process(int x) const override {
std::cout << "Middle: " << x * 2 << '\n';
}
// final: no further derived class can override execute()
void execute() final { std::cout << "Middle::execute\n"; }
};
class Bottom : public Middle {
public:
void process(int x) const override {
std::cout << "Bottom: " << x * 3 << '\n';
}
// ERROR if uncommented: execute() is final in Middle
// void execute() override { }
};
// A final class cannot be inherited from at all
class Leaf final : public Base {
public:
void process(int x) const override {
std::cout << "Leaf: " << x << '\n';
}
};
// ERROR if uncommented: cannot inherit from final class
// class Attempt : public Leaf {};
int main() {
Bottom b;
Base& ref = b;
ref.process(10); // Bottom: 30 (virtual dispatch)
ref.execute(); // Middle::execute (final version)
return 0;
}Abstract Classes & NVI Pattern
A class with at least one pure virtual function (= 0) is abstract — it cannot be instantiated. The Non-Virtual Interface (NVI) pattern provides a public non-virtual entry point that calls a private virtual function, letting the base class control pre/post conditions.
#include <iostream>
#include <string>
// NVI Pattern: public non-virtual calls private virtual
class Validator {
public:
virtual ~Validator() = default;
// Public non-virtual interface — controls the workflow
bool validate(const std::string& input) const {
if (input.empty()) {
std::cout << "[Validator] Input is empty\n";
return false;
}
// Delegate to derived class implementation
bool result = do_validate(input);
std::cout << "[Validator] " << (result ? "PASS" : "FAIL") << '\n';
return result;
}
private:
// Pure virtual — derived classes MUST implement
virtual bool do_validate(const std::string& input) const = 0;
};
class EmailValidator : public Validator {
bool do_validate(const std::string& input) const override {
return input.find('@') != std::string::npos
&& input.find('.') != std::string::npos;
}
};
class LengthValidator : public Validator {
std::size_t min_len_;
bool do_validate(const std::string& input) const override {
return input.length() >= min_len_;
}
public:
explicit LengthValidator(std::size_t min) : min_len_{min} {}
};
int main() {
EmailValidator email_v;
LengthValidator len_v{5};
email_v.validate("user@example.com"); // PASS
email_v.validate("invalid"); // FAIL
len_v.validate("hi"); // FAIL
len_v.validate("hello!"); // PASS
return 0;
}RTTI & dynamic_cast
RTTI (Run-Time Type Information) lets you query the actual type of a polymorphic object at runtime. dynamic_cast safely casts a base pointer to a derived pointer, returning nullptr if the cast fails. For references, a failed dynamic_cast throws std::bad_cast.
typeid(obj) returns a std::type_info reference that can be compared to identify the runtime type.
Use RTTI sparingly — frequent dynamic_cast usage often signals a design problem. Prefer virtual functions for type-specific behavior.
Never call virtual functions from a constructor or destructor. During construction, the object's dynamic type is the class currently being constructed, not the final derived class. A virtual call in Base::Base() will call Base::method(), not Derived::method() — even if the object being created is a Derived. This is a well-known source of bugs that can be extremely difficult to diagnose.
- Virtual functions enable runtime polymorphism via vtables and vptrs
- Any class with virtual functions must have a
virtualdestructor - Always use
overridewhen overriding — it catches signature mismatches at compile time - Pure virtual functions (
= 0) make a class abstract and force derived classes to implement them - The NVI pattern lets base classes enforce pre/post conditions around customization points
- Avoid calling virtual functions in constructors/destructors — the derived class is not yet (or no longer) fully constructed
Quiz — Test Your Knowledge
(20 XP)1. What happens if a polymorphic base class does NOT have a virtual destructor?
2. What does the `override` keyword do?
3. Why should you avoid calling virtual functions from a constructor?