Skip to content
C++
Language
since C++98
Intermediate

Virtual Functions and Polymorphism

C++ virtual functions, vtables, dynamic dispatch, override, final, abstract classes, virtual destructors, and performance tradeoffs.

Virtual Functionssince C++98

A virtual function is a member function declared with the virtual keyword that participates in dynamic dispatch β€” the runtime selection of the correct override based on the actual type of the object, not the static type of the pointer or reference through which it is called.

Overview

When you call a non-virtual member function through a base pointer, the compiler resolves the call at compile time from the declared type. A virtual call defers that resolution: the runtime follows the object's hidden vptr to the class's vtable β€” a per-class array of function pointers β€” and invokes the correct slot. This one level of indirection enables open-ended extensibility: code written against Base* correctly dispatches to any derived override, including overrides compiled weeks later.

Every class that declares or inherits at least one virtual function is a polymorphic class. The compiler injects a vptr (typically at offset zero) into each instance. A call p->foo() compiles roughly to (*p->vptr[foo_slot])(p). The cost is one pointer load, one indexed load from the vtable, and an indirect call β€” typically 1–4 cycles on modern hardware when the branch target buffer is warm. The real expense is the prevention of inlining, since the compiler generally cannot inline through an indirect call without proof of the concrete type.

C++11 added override and final specifiers that make intent explicit and enable compiler enforcement. Always use override on every override; without it, a signature mismatch silently creates a new non-virtual function rather than overriding the intended one.

Syntax

cpp
struct Base {
    virtual void foo();              // virtual with definition
    virtual void bar() = 0;          // pure virtual β€” Base is now abstract
    virtual Base* clone() const;     // return type may be narrowed in overrides
    virtual ~Base() = default;       // virtual destructor β€” required if Base* is ever deleted
};

struct Derived : Base {
    void foo() override;             // C++11: compiler verifies signature matches Base::foo
    void bar() override;             // must override β€” bar is pure virtual in Base
    Derived* clone() const override; // C++11: covariant return β€” Derived* is-a Base*
    void baz() final;                // C++11: no further overrides of baz in subclasses
};

struct Leaf final : Derived { };     // C++11: cannot be used as a base class

override without a matching virtual in the base is a hard error, catching typos, missing const, and base-function renames before they silently regress. final on a class allows the compiler to devirtualize all calls through that static type unconditionally, since no further derived class can override the vtable slots.

Examples

Basic polymorphic dispatch

cpp
#include <memory>
#include <vector>
#include <print>    // C++23

struct Shape {
    virtual double area() const = 0;
    virtual ~Shape() = default;
};

struct Circle final : Shape {
    explicit Circle(double r) : r_{r} {}
    double area() const override { return 3.14159265358979 * r_ * r_; }
private:
    double r_;
};

struct Rectangle final : Shape {
    Rectangle(double w, double h) : w_{w}, h_{h} {}
    double area() const override { return w_ * h_; }
private:
    double w_, h_;
};

void print_areas(const std::vector<std::unique_ptr<Shape>>& shapes) {
    for (const auto& s : shapes)
        std::println("area = {:.4f}", s->area());  // dynamic dispatch each iteration
}

Non-Virtual Interface (NVI) pattern

Public functions are non-virtual; customization points are private virtual. This lets the base class enforce invariants, ordering, and logging without requiring derived classes to call Base::foo() chains:

cpp
struct Codec {
    // Stable, non-virtual public API
    std::vector<uint8_t> encode(std::span<const uint8_t> data) {  // std::span: C++20
        validate_input(data);
        auto out = do_encode(data);  // virtual hook β€” private
        append_checksum(out);
        return out;
    }

private:
    virtual std::vector<uint8_t> do_encode(std::span<const uint8_t> src) = 0;
    void validate_input(std::span<const uint8_t> data) { /* precondition checks */ }
    void append_checksum(std::vector<uint8_t>& buf)    { /* CRC32 append */ }
};

struct ZstdCodec final : Codec {
private:
    std::vector<uint8_t> do_encode(std::span<const uint8_t> src) override {
        // compress with zstd, return result
        return {};
    }
};

Pure virtual with a default body

A pure virtual function makes the class abstract, but may still carry a base implementation callable via qualified name. This is useful when derived classes want to extend rather than replace the base behavior:

cpp
struct Logger {
    virtual void write(std::string_view msg) = 0;
    virtual ~Logger() = default;
};

void Logger::write(std::string_view msg) {
    // Baseline: timestamp prefix, flush β€” subclasses may call this
}

struct FileLogger final : Logger {
    void write(std::string_view msg) override {
        Logger::write(msg);   // explicit qualified call to base body
        // then write to file_
    }
private:
    std::ofstream file_;
};

vtable layout and sizeof

cpp
struct Empty {};
struct WithVirtual { virtual void f() {} };

// On a 64-bit target:
static_assert(sizeof(Empty)       == 1);   // minimum non-zero size
static_assert(sizeof(WithVirtual) == 8);   // one vptr injected at offset 0

// Every derived class has its own vtable; shared base slots are overwritten
struct A { virtual void x() {} virtual void y() {} };
struct B : A { void x() override {} };
// B's vtable: [B::x, A::y]  β€” slot 0 overwritten, slot 1 inherited

Best Practices

Always declare a virtual destructor in any class you intend to be deleted through a base pointer. Omitting it makes delete base_ptr undefined behavior for any derived type β€” the derived destructor is never invoked, resources leak, and compilers are permitted (and sometimes do) generate completely wrong behavior. virtual ~Base() = default; is zero-cost and eliminates the UB entirely.

Use override on every override without exception. It is the single most impactful virtual-related practice: it converts silent new-function creation into a compile error, survives base-class refactors, and documents intent.

Mark leaf classes final when derivation is not part of the design. This communicates API contract and gives the optimizer a devirtualization opportunity for any call site where the concrete type is statically known.

Prefer the NVI pattern in library code. Separating the stable public API (non-virtual) from the customization points (private virtual) keeps invariants out of derived-class hands and avoids fragile base-class chains.

Take polymorphic arguments by reference or pointer, never by value. Passing by value copies into a Base subobject and slices the derived portion irreversibly.

Common Pitfalls

Virtual dispatch does not work inside constructors or destructors

During construction of a Base subobject, the vptr points to Base's vtable because the derived portion does not yet exist. Calls to virtual functions in a constructor always dispatch to the most-derived type constructed so far, which is typically the base β€” not what callers expect:

cpp
struct Base {
    Base() { setup(); }               // vptr = Base's vtable here
    virtual void setup() { std::println("Base::setup"); }
};

struct Derived : Base {
    void setup() override { std::println("Derived::setup"); }
};

Derived d;  // prints "Base::setup" β€” Derived::setup never called from ctor

The fix is a two-phase initialization: make the constructor do minimal work and expose a non-virtual initialize() that callers invoke after construction, or use a factory function.

Object slicing

Assigning or copying a derived object into a base object strips all derived state. The resulting object's vptr points to the base vtable:

cpp
void process(Shape s) {   // by value β€” slices Circle/Rectangle to Shape
    s.area();             // Shape::area() is pure virtual β€” undefined behavior
}

Circle c{3.0};
process(c);  // slices; the Circle part is gone

void process(const Shape& s) { s.area(); }  // correct: reference preserves dynamic type

Signature drift without override

If a base class renames or changes the signature of a virtual function, derived classes without override silently become non-virtual functions that shadow instead of override:

cpp
struct Base { virtual void handle(int);  };

// Base refactored to: virtual void handle(long);
struct Derived : Base {
    void handle(int) override;  // error: no matching virtual β€” caught immediately
    // Without override: silently becomes a new non-virtual Derived::handle(int)
};

Templates cannot be virtual

The vtable size must be fixed at class definition time, but template instantiation is open-ended. A function template cannot be virtual:

cpp
struct Base {
    template<typename T>
    virtual void process(T);  // error: member function template cannot be virtual
};

For type-parameterized runtime dispatch, use type erasure (std::any, std::function, or a virtual function taking an erased type) instead.

Virtual functions and multiple inheritance: ambiguous overrides

When two bases declare a function with the same name, a singly-defined override in the derived class may be ambiguous:

cpp
struct A { virtual void f() {} };
struct B { virtual void f() {} };

struct C : A, B {
    void f() override {}   // overrides both A::f and B::f β€” intentional, no ambiguity here
};

// Ambiguity arises at the call site when C is not the declared type:
A* a = new C{};
a->f();  // OK: A's vtable slot dispatches to C::f
B* b = new C{};
b->f();  // OK: B's vtable slot also dispatches to C::f

Diamond hierarchies require virtual inheritance to avoid base subobject duplication, which adds another level of pointer indirection and is rarely the right design choice.

Performance Considerations

Devirtualization β€” the compiler replacing an indirect virtual call with a direct or inlined call β€” is the primary optimization lever:

cpp
// 1. final class: compiler knows Leaf has no further overrides
void dispatch(Leaf& obj) { obj.foo(); }     // may be devirtualized and inlined

// 2. Local concrete variable: static type is exact
Concrete c;
c.foo();   // devirtualized β€” no vtable lookup

// 3. CRTP: zero-overhead static polymorphism for performance-critical code
template<typename Derived>
struct ProcessorBase {
    void run() { static_cast<Derived*>(this)->do_run(); }  // inlined, no vptr
};

struct FastProcessor : ProcessorBase<FastProcessor> {
    void do_run() { /* hot path */ }
};

Profile before eliminating virtual dispatch. Modern branch target buffers handle monomorphic and bimorphic call sites well, and the actual overhead is often dominated by cache misses in the objects themselves, not the vtable indirection.

vtable layout and vptr mechanics

Every polymorphic class instance carries a hidden vptr that the compiler manages automatically. The vptr is inserted at a compiler-chosen offset (virtually always offset 0 on modern ABI targets) and points to a per-class read-only array of function pointers β€” the vtable. The vtable is emitted once per class, not once per object, so the per-instance cost is exactly one pointer.

cpp
// C++11
// Illustrative ABI model β€” actual layout is implementation-defined
struct Animal {
    /* vptr (8 bytes on 64-bit) */ // injected by compiler at offset 0
    int age;                       // offset 8
};

struct Dog : Animal {
    int breed_id;                  // offset 12 (follows inherited Animal layout)
};

// Dog's vtable slots = [Dog::speak, Dog::move, ...]
// Animal's vtable slots = [Animal::speak, Animal::move, ...]
// When Dog overrides speak(), its vtable replaces the Animal::speak pointer.

With multiple inheritance each base subobject has its own vptr, since each base's vtable has an independent slot numbering. Under virtual inheritance an additional level of indirection (a vbase offset) is added to locate the shared base subobject. This is a key reason to avoid deep MI hierarchies β€” the per-call overhead grows and devirtualization becomes harder.

cpp
// C++11
struct A { virtual void f(); };
struct B { virtual void g(); };
struct C : A, B { void f() override; void g() override; };

// sizeof(C) == sizeof(vptr_A) + sizeof(vptr_B) + padding
// C has two vtable sections; the B-subobject vptr points to a thunk that
// adjusts 'this' before forwarding to C::g.
static_assert(sizeof(C) == 16);  // 2 vptrs, no data members β€” typical 64-bit result

Pure virtual functions and abstract classes

A pure virtual declaration (= 0) makes the class abstract, preventing direct instantiation while mandating that concrete subclasses provide an implementation. Abstract classes define an interface contract; any subclass that does not override every pure virtual slot is itself abstract.

cpp
// C++11
struct Serializable {
    virtual void serialize(std::ostream& out) const = 0;
    virtual void deserialize(std::istream& in)      = 0;
    virtual ~Serializable() = default;
};

// Concrete β€” overrides both slots
struct Config final : Serializable {
    void serialize(std::ostream& out) const override { out << data_; }
    void deserialize(std::istream& in)      override { in  >> data_; }
private:
    std::string data_;
};

// Still abstract β€” missing deserialize
struct ReadOnlyConfig : Serializable {
    void serialize(std::ostream& out) const override { out << "ro"; }
    // deserialize not overridden β†’ ReadOnlyConfig is still abstract
};

// Serializable s;       // error: cannot instantiate abstract class
// ReadOnlyConfig roc;   // error: same
Config c;                // OK

A pure virtual function may still have a body, defined out-of-line. Derived classes can call it with a qualified name to reuse the base implementation while still being required to provide their own override.

cpp
// C++11
struct Protocol {
    virtual void handshake() = 0;
    virtual ~Protocol() = default;
};

void Protocol::handshake() {
    // default TLS negotiation β€” shared base behavior
}

struct TLSv13 final : Protocol {
    void handshake() override {
        Protocol::handshake();  // reuse base logic
        // then perform TLS 1.3 specifics
    }
};

Virtual destructor rules

Delete a polymorphic object through a base pointer without a virtual destructor and you have undefined behavior β€” the derived destructor will not run. This is one of the most consequential rules in C++ because the symptoms (resource leaks, partial destruction, memory corruption) are silent and non-deterministic.

cpp
// C++98
struct Resource {
    Resource() { /* acquire */ }
    ~Resource() { /* release β€” but never called if deleted through Base* */ }
};

struct Base {
    ~Base() {}  // NOT virtual β€” dangerous if Base* ever owns a Derived
};

struct Derived : Base {
    Resource res;
};

Base* p = new Derived{};
delete p;  // undefined behavior: Derived::~Derived never called, res leaks
cpp
// C++11
struct Base {
    virtual ~Base() = default;  // correct: ensures Derived::~Derived is called
};

struct Derived : Base {
    std::vector<int> data;      // correctly destroyed through Base*
};

Base* p = new Derived{};
delete p;  // OK: virtual dispatch reaches Derived::~Derived, then Base::~Base

If you intend a base class to be non-owning (used only as a non-owning observer), declare the destructor protected and non-virtual instead. This prevents delete base_ptr from compiling at all while avoiding the vtable slot cost.

cpp
// C++11
struct Observer {
protected:
    ~Observer() = default;        // non-virtual, protected β€” prevents delete-via-base
public:
    virtual void on_event() = 0;
};

Covariant return types

An override may narrow the return type to a pointer or reference to a class derived from the base function's return type. This is the only permitted relaxation of the "override must match signature exactly" rule and allows factory-style interfaces to return more specific types without casts.

cpp
// C++98
struct Document {
    virtual Document* clone() const;
    virtual ~Document() = default;
};

struct SpreadSheet : Document {
    SpreadSheet* clone() const override;  // covariant: SpreadSheet* is-a Document*
};

// Caller with a Document* gets a Document* β€” no information lost
// Caller with a SpreadSheet& gets a SpreadSheet* β€” no cast required
SpreadSheet s;
SpreadSheet* copy = s.clone();  // no static_cast needed

Covariance applies only to pointer and reference returns, and the derived return type must be publicly and unambiguously derived from the base return type. It does not work with smart pointers because std::unique_ptr<Derived> is not derived from std::unique_ptr<Base>.

cpp
// C++11
struct Base {
    // virtual std::unique_ptr<Base> clone() const = 0;  // OK as interface
};

struct Derived : Base {
    // std::unique_ptr<Derived> clone() const override;  // NOT covariant β€” ill-formed
    // Must return std::unique_ptr<Base> and rely on static_pointer_cast at call site
};

Devirtualization by the compiler

The compiler can eliminate a virtual dispatch entirely when it can prove the concrete type at compile time. Devirtualization converts an indirect call into a direct call and, crucially, enables inlining β€” which cascades into further constant propagation and dead-code elimination.

cpp
// C++11
struct Widget {
    virtual void draw() const;
};

struct Button final : Widget {   // final: no further overrides possible
    void draw() const override;
};

void render(Button& b) {
    b.draw();   // devirtualized: compiler sees Button is final, emits direct call
}

void render(Widget& w) {
    w.draw();   // NOT devirtualized: w could be any Widget subtype
}

Profile-guided optimization (PGO) can devirtualize polymorphic call sites that are hot and monomorphic at runtime, inserting a speculative type check and a direct fast path. This is common in JVM-style VMs and is increasingly used by Clang and GCC's PGO pipelines for C++.

cpp
// C++17
// Compiler may emit code equivalent to:
//   if (__builtin_expect(w.vptr == &Button::vtable, 1))
//       Button::draw(&w);    // inlined fast path
//   else
//       w.vptr[draw_slot](&w); // slow path

// Use -fprofile-use (GCC/Clang) or /GL + /LTCG (MSVC) to enable PGO devirt.

Virtual functions and inline

Declaring a virtual function inline is legal but only hints to the compiler β€” the virtual call itself cannot be inlined through an indirect dispatch. The inline specifier on a virtual function has two effects: it permits the definition to appear in multiple translation units (the ODR relaxation), and it enables the compiler to inline the call when the concrete type is statically known.

cpp
// C++11
struct Matrix {
    // inline + virtual: definition in header is ODR-safe
    virtual inline double det() const { return 0.0; }
    virtual ~Matrix() = default;
};

struct Identity final : Matrix {
    double det() const override { return 1.0; }  // trivially inlinable when type known
};

Identity id;
double d = id.det();  // inlined: static type is Identity, final class, no dispatch
Matrix* m = &id;
double e = m->det();  // NOT inlined: goes through vtable even though body is inline

Defining virtual functions in the class body implicitly marks them inline, which is fine for short accessors but can cause code bloat if the function is large, since every translation unit that sees the class header may emit a copy.

NVI (Non-Virtual Interface) pattern

The NVI pattern makes public member functions non-virtual and routes customization through private (or protected) virtual hooks. This separates the stable, documented contract (the public non-virtual surface) from the extension mechanism (the private virtual slots), allowing the base class to enforce pre/post conditions, logging, and sequencing without relying on derived classes to call Base::foo().

cpp
// C++11
class Connection {
public:
    // Stable public API β€” never virtual
    bool open(std::string_view address) {
        log_attempt(address);
        if (!do_open(address)) return false;  // virtual hook
        on_connected();                        // virtual hook
        return true;
    }

    void close() noexcept {
        do_close();     // virtual hook
        log_closed();
    }

    // Non-copyable, non-movable for simplicity
    Connection()                           = default;
    virtual ~Connection()                  = default;
    Connection(const Connection&)          = delete;
    Connection& operator=(const Connection&) = delete;

private:
    virtual bool do_open(std::string_view address) = 0;
    virtual void on_connected() {}   // optional hook with default no-op
    virtual void do_close() noexcept = 0;

    void log_attempt(std::string_view address) { /* structured log */ }
    void log_closed()                          { /* structured log */ }
};

class TcpConnection final : Connection {
private:
    bool do_open(std::string_view address) override {
        // TCP connect logic
        return true;
    }
    void do_close() noexcept override {
        // TCP shutdown logic
    }
};

NVI is especially valuable in library code where derived classes are written by external users: the base enforces invariants unconditionally and external authors only implement the narrow hook interface. It also avoids the fragile base-class problem where forgetting Base::foo() in an override silently skips essential setup.

See Also