Table of Contents
- Virtual Functions
- override and final
- Pure Virtual Functions and Abstract Classes
- Virtual Destructors
- The vtable and vptr
- Static vs. Dynamic Dispatch
- Construction and Destruction Order
- The Slicing Problem
- Multiple Inheritance
- The Diamond Problem and Virtual Inheritance
- RTTI and dynamic_cast
- Pure Interfaces and the Non-Virtual Interface Idiom
- Virtual vs. CRTP — When to Pick Which
- Trait-Style Polymorphism (Type Erasure)
- Common Pitfalls
Polymorphism lets a single interface drive different concrete behaviors. Modern C++ supports three distinct flavors:
- Subtype polymorphism — inheritance and
virtualfunctions, resolved at run time through a per-object pointer to a table of function pointers (the vtable). Concrete types share a common base class. - Static (parametric) polymorphism — templates, overloading, CRTP, and concepts, resolved at compile time. Zero indirection, full inlining.
- Trait-style polymorphism — type erasure. Unrelated concrete types are
stored behind a value-semantic wrapper that calls into a hidden vtable.
No shared base class is required; the contract is purely behavioral. This
is the C++ analog of Rust's
dyn Trait.
This chapter focuses on the first flavor, the vtable mechanics that implement it, and the pitfalls of class hierarchies — then revisits CRTP and type erasure at the end so you can pick the right tool.
For the static counterpart — CRTP, concepts, and template-based dispatch — see :doc:`cpp_template` and :doc:`cpp_concepts`. For Rust's trait model, see :doc:`../rust/rust_traits`. For the safe runtime-checked downcast, see :doc:`cpp_casting`.
| Source: | src/polymorphism/virtual-functions |
|---|
A member function declared virtual in a base class can be overridden in a
derived class. When called through a pointer or reference to the base, the
dynamic type of the object decides which override runs. Without virtual,
the call is bound to the static type at compile time.
#include <iostream>
struct Animal {
virtual void speak() const { std::cout << "..." << "\n"; }
void name() const { std::cout << "Animal" << "\n"; } // non-virtual
};
struct Dog : Animal {
void speak() const override { std::cout << "Woof" << "\n"; }
void name() const { std::cout << "Dog" << "\n"; }
};
int main() {
Dog d;
Animal &a = d;
a.speak(); // "Woof" - dynamic dispatch through vtable
a.name(); // "Animal" - static dispatch, base version called
}The non-virtual name is hidden in Dog, not overridden. The choice of
which to call is made by the compiler from the static type, so a.name()
calls Animal::name even though a actually refers to a Dog.
| Source: | src/polymorphism/override-final |
|---|
override (C++11) tells the compiler that a function is intended to override
a base virtual. If the signature does not actually match a base virtual, the
program fails to compile. This catches typos, forgotten const qualifiers,
and signature drift after a refactor.
final prevents further overriding (when applied to a function) or further
derivation (when applied to a class). It also enables a useful optimization:
the compiler can devirtualize calls to a final override.
struct Base {
virtual void f() const;
virtual void g(int);
};
struct Derived : Base {
void f() const override; // OK
// void f() override; // ERROR: missing const
void g(int) override final; // cannot be overridden further
};
struct Sealed final : Derived { // cannot be derived from
void f() const override;
};Always write override on overriding functions. It is not redundant — it is
a compile-time contract that the override still tracks the base.
| Source: | src/polymorphism/pure-virtual |
|---|
A function with = 0 is pure virtual; the class that declares it is
abstract and cannot be instantiated. Derived classes must override every
pure virtual function before they become concrete.
struct Shape {
virtual double area() const = 0;
virtual ~Shape() = default; // see Virtual Destructors
};
struct Circle : Shape {
explicit Circle(double r) : r_(r) {}
double area() const override { return 3.14159 * r_ * r_; }
private:
double r_;
};
// Shape s; // ERROR: cannot instantiate abstract class
Circle c(1.0); // OK
Shape *p = &c; // OK: pointer to abstract baseA pure virtual function can still have a definition; derived classes that
explicitly call Base::fn() will then run that definition. This is
occasionally useful for shared default behavior in an otherwise abstract
interface.
| Source: | src/polymorphism/virtual-destructor |
|---|
If a class is meant to be a polymorphic base — that is, deleted through a
base pointer — its destructor must be virtual. Without it, deleting
through the base pointer invokes only the base destructor, leaking the
derived part of the object. This is undefined behavior.
struct Base {
~Base() { /* not virtual */ }
};
struct Derived : Base {
std::vector<int> data_{1, 2, 3};
~Derived() { /* destructor never runs */ }
};
Base *p = new Derived;
delete p; // UB: ~Derived not called, data_ leaksTwo safe defaults:
- If a class has any virtual function, give it a
virtual ~Base() = default;. - If a class is not intended for polymorphic deletion, mark it
finalor document that fact, so callers do not silently rely on a non-virtual destructor.
Most compilers implement dynamic dispatch with a vtable: a per-class array of function pointers, one slot per virtual function. Each polymorphic object stores a hidden vptr pointing to its class's vtable. A virtual call becomes two indirections: load the vptr, index into the vtable, jump to the target.
struct B {
virtual void f(); // vtable slot 0
virtual void g(); // vtable slot 1
int x;
};
struct D : B {
void f() override; // overrides slot 0
void h(); // not virtual
int y;
};
// Memory layout (typical Itanium ABI):
//
// B object: D object:
// +-------+ +-------+
// | vptr | ----> | vptr | ----> D's vtable: [&D::f, &B::g, ...]
// | x | | x |
// +-------+ | y |
// +-------+Consequences of this layout:
- Every polymorphic object pays one pointer (typically 8 bytes on 64-bit) of storage for the vptr.
- A virtual call is one extra load and an indirect branch compared to a direct call. The cost is small but real, and it can defeat inlining.
- The vptr is set by each constructor as the object's type changes during construction (see Construction and Destruction Order).
The exact layout is ABI-defined. The Itanium ABI used by GCC, Clang, and the Apple toolchain places the vptr at offset 0 and shares vtables between translation units; MSVC's ABI differs in the details (and in how it handles multiple inheritance) but follows the same general scheme.
You can inspect the layout with -fdump-lang-class (GCC) or
-Xclang -fdump-record-layouts (Clang).
A non-virtual call is bound at compile time and can be inlined. A virtual call
requires the dynamic type, so it can only be devirtualized when the compiler
proves the dynamic type — for example, when the object's most-derived type
is visible at the call site, or when the function is declared final.
struct I { virtual int f() const = 0; };
struct A final : I { int f() const override { return 1; } };
int callI(const I &i) { return i.f(); } // virtual call
int callA(const A &a) { return a.f(); } // devirtualized: A is final
int callD() { A a; return a.f(); } // direct call, inlinableRules of thumb:
- Calls through a base pointer or reference are virtual.
- Calls on a known concrete object are direct.
finalon the override (or the class) lets the compiler devirtualize.- Inside a constructor or destructor, virtual calls dispatch to the currently-constructed type, not the most-derived type (see below).
| Source: | src/polymorphism/ctor-dtor-dispatch |
|---|
During construction, the object's vptr is set to each base subobject's vtable in turn before its own constructor body runs. So a virtual call from inside a base constructor dispatches to the base version, not the derived override — the derived part of the object does not exist yet. Destruction is the mirror image.
struct B {
B() { f(); } // calls B::f, not D::f
virtual void f() { std::puts("B::f"); }
virtual ~B() { f(); } // calls B::f
};
struct D : B {
void f() override { std::puts("D::f"); }
};
D d; // prints "B::f" twice (ctor and dtor)This is a common interview question and a real source of bugs. Never call
virtual functions from a constructor or destructor expecting derived
behavior. If you need polymorphic initialization, use a two-step
create factory or a separate init call after construction.
| Source: | src/polymorphism/slicing |
|---|
Assigning a derived object to a base value copies only the base subobject — the derived part is "sliced off". The result is a base object that has lost its dynamic type, and any virtual call on it will run the base version.
struct Animal { virtual void speak() const { std::puts("..."); } };
struct Dog : Animal { void speak() const override { std::puts("Woof"); } };
void by_value(Animal a) { a.speak(); } // sliced -> "..."
void by_reference(const Animal &a) { a.speak(); } // polymorphic -> "Woof"
int main() {
Dog d;
by_value(d);
by_reference(d);
}To preserve polymorphism, pass by reference (const Animal &), pointer
(const Animal *), or a smart pointer (std::unique_ptr<Animal>).
Containers of polymorphic objects must hold pointers, not values:
std::vector<Animal> slices on every push_back;
std::vector<std::unique_ptr<Animal>> does not.
| Source: | src/polymorphism/multiple-inheritance |
|---|
A class may have more than one direct base. Each non-virtual base contributes its own subobject and (if polymorphic) its own vptr. Casting between base pointers may adjust the pointer value to land on the right subobject.
struct Drawable { virtual void draw() const = 0; virtual ~Drawable() = default; };
struct Serializable { virtual void save() const = 0; virtual ~Serializable() = default; };
struct Widget : Drawable, Serializable {
void draw() const override;
void save() const override;
};
Widget w;
Drawable *d = &w; // points at Drawable subobject
Serializable *s = &w; // points at Serializable subobject (offset!)
// static_cast<void*>(d) != static_cast<void*>(s) in generalMultiple inheritance is fine for interface inheritance (abstract bases with no state). It gets complicated quickly when two bases share a common ancestor — the diamond problem.
| Source: | src/polymorphism/diamond |
|---|
When a class indirectly inherits from the same base through two paths, the default is to embed two copies of that base. Member access becomes ambiguous, and the two copies can diverge.
struct A { int x; };
struct B : A {};
struct C : A {};
struct D : B, C {}; // two A subobjects
D d;
// d.x; // ERROR: ambiguous
d.B::x = 1;
d.C::x = 2; // independent of d.B::xvirtual inheritance collapses the duplicates into a single shared
subobject:
struct A { int x; };
struct B : virtual A {};
struct C : virtual A {};
struct D : B, C {}; // one A subobject
D d;
d.x = 1; // unambiguousVirtual inheritance has costs: an extra indirection (the vbase pointer) to locate the shared subobject, and the most-derived class becomes responsible for initializing the virtual base. Reach for it only when you genuinely need shared state across diamond paths; prefer composition or interface-only multiple inheritance otherwise.
Run-Time Type Information (RTTI) lets you query the dynamic type of a
polymorphic object. typeid returns a std::type_info; dynamic_cast
performs a checked downcast.
#include <typeinfo>
struct Base { virtual ~Base() = default; };
struct Derived : Base { void special(); };
void handle(Base *b) {
if (auto *d = dynamic_cast<Derived *>(b)) { // null on failure
d->special();
}
try {
Derived &dr = dynamic_cast<Derived &>(*b); // throws std::bad_cast
dr.special();
} catch (const std::bad_cast &) {
}
}dynamic_cast requires a polymorphic source type (at least one virtual
function in the static type). It is not free: it walks the type hierarchy at
run time. If you find yourself reaching for it often, the design probably
wants another virtual function instead.
For the cast hierarchy (static_cast, const_cast, reinterpret_cast)
see :doc:`cpp_casting`.
| Source: | src/polymorphism/nvi |
|---|
A common pattern is to expose a non-virtual public API that calls into private virtual hooks. The base class fixes pre/post conditions; derived classes only customize behavior.
struct Logger {
void log(std::string_view msg) { // public, non-virtual
prefix();
write(msg); // protected/private virtual
flush();
}
virtual ~Logger() = default;
private:
void prefix() { /* ... */ }
virtual void write(std::string_view) = 0;
void flush() { /* ... */ }
};This Non-Virtual Interface (NVI) idiom keeps the contract in one place and makes the override surface explicit.
| Source: | src/template/crtp |
|---|
If you do not need heterogeneous containers or runtime dispatch, the Curiously Recurring Template Pattern gives polymorphism without a vtable and inlines cleanly:
template <typename Derived>
struct Shape {
double area() const {
return static_cast<const Derived &>(*this).area_impl();
}
};
struct Circle : Shape<Circle> {
double area_impl() const { return 3.14159 * r_ * r_; }
double r_;
};Choose dynamic polymorphism when:
- You need to store mixed concrete types in one collection.
- The set of derived types is not known at the call site.
- The runtime cost of an indirect call is negligible compared to the work inside the function.
Choose static polymorphism (CRTP, concepts, templates) when:
- All types are known at compile time.
- The function is small and benefits from inlining.
- You want zero-overhead abstractions.
See :doc:`cpp_template` for CRTP details and :doc:`cpp_concepts` for the modern concepts-based alternative.
The third flavor of polymorphism — the one Rust makes idiomatic with
dyn Trait — is type erasure. Unrelated concrete types are stored
behind a value-semantic wrapper that calls into a hidden vtable; no shared
base class is required.
std::vector<AnyDrawable> shapes;
shapes.emplace_back(Circle{1.0}); // no inheritance from anything
shapes.emplace_back(Square{2.0});This pattern is large enough that it has its own chapter:
see :doc:`cpp_type_erasure` for the Concept/Model recipe, small-buffer
optimization, std::function / std::any /
std::move_only_function, and the comparison with Rust trait objects.
- Non-virtual destructor in a polymorphic base. Deleting through a base
pointer is undefined behavior. Always make the destructor
virtualorprotected(the latter prevents base-pointer deletion entirely). - Calling virtual functions from constructors or destructors. Dispatch uses the type currently under construction, not the most-derived type.
- Slicing on copy. Storing or passing polymorphic objects by value silently drops their derived state.
- Forgetting
override. Without it, a signature change in the base silently turns an override into a new function. - Default argument values. Defaults are picked from the static type of
the call, not the dynamic type.
base->f()usesBase's defaults even if the override declares different ones — confusing and a frequent source of bugs. Avoid changing defaults in overrides. - Calling
dynamic_castin a hot loop. It walks the type hierarchy. Cache the result, or refactor to add a virtual function. - Virtual inheritance "just to be safe". It adds storage and indirection. Use it only for genuine diamonds with shared state.