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.

See Also