Skip to content

Virtual Functions and vtables

Virtual Functions and Dispatch Tables (vtables)

Section titled “Virtual Functions and Dispatch Tables (vtables)”

Virtual functions are the foundation of runtime polymorphism in C++. When a member function is Declared virtualThe call is resolved at runtime based on the object”s dynamic type through the Vtable dispatch mechanism. This section covers the virtual keyword, the vptr/vtable ABI, dispatch Costs, and the final/override specifiers.

A member function declared virtual in a base class enables dynamic dispatch: the function Called depends on the runtime type of the object, not the static type of the pointer or Reference through which it is invoked [N4950 S13.3.2].

#include <iostream>
struct Base {
virtual void greet() const {
std::cout << "Base\n";
}
virtual ~Base() = default;
};
struct Derived : Base {
void greet() const override {
std::cout << "Derived\n";
}
};
int main() {
Base b;
Derived d;
Base& ref_to_d = d;
ref_to_d.greet();
Base* ptr_to_d = &d;
ptr_to_d->greet();
}

Output:

Derived
Derived

When greet() is called through a Base& or Base* that actually refers to a Derived object, The virtual dispatch mechanism selects Derived::greet. This selection occurs at runtime, not At compile time.

When a virtual member function is called, the function to be called is determined by the dynamic Type of the object expression. If the object expression is a dereferenced pointer or a reference, The dynamic type is the type of the most-derived object that the pointer or reference denotes. The Standard prescribes the observable behavior but does not mandate any particular implementation Mechanism.

The implementation of virtual dispatch is not specified by the C++ Standard — the Standard only Prescribes the observable behavior. However, virtually every mainstream compiler (GCC, Clang, MSVC) uses the vtable (virtual function table) model, codified in the Itanium C++ ABI.

The mechanism works as follows:

  1. One vtable per polymorphic class. The compiler generates a hidden static array of function pointers — the vtable — for every class that has at least one virtual function or inherits from a polymorphic class.

  2. One vptr per object instance. Every object of a polymorphic class contains a hidden pointer (the vptr) to its class’s vtable. The vptr is set by the constructor.

  3. Dispatch is a two-step indirect call: When a virtual function f is called through a pointer pThe compiler emits code equivalent to (*p->vptr[offset_of_f])(p). This is a single indirect function call.

\mathrm{dispatch cost = 1 \mathrm{ memory load (vptr) + 1 \mathrm{ indexed load (function pointer) + 1 \mathrm{ indirect call
ComponentDescription
vtableStatic array of function pointers; one per class
vptrHidden pointer in each object; points to the class’s vtable
SlotEach virtual function occupies a fixed index in the vtable
ThunksCompiler-generated stubs that adjust this before dispatch

:::note ABI Note The Itanium C++ ABI (used by GCC and Clang on all platforms except Windows) Mandates that the vptr is at offset 0 within the object (before any data members). MSVC uses a Similar but incompatible layout on Windows. :::

For a class hierarchy:

struct Base {
virtual void f();
virtual void g();
virtual ~Base();
};
struct Derived : Base {
void f() override;
void g() override;
~Derived() override;
virtual void h();
};

The vtable layout under the Itanium ABI is:

Base::vtable:
[0] &Base::~Base (complete object destructor)
[1] &Base::f
[2] &Base::g
[3] &std::type_info for Base
Derived::vtable:
[0] &Derived::~Derived (complete object destructor)
[1] &Derived::f (overrides Base::f)
[2] &Derived::g (overrides Base::g)
[3] &Derived::h (new virtual function)
[4] &std::type_info for Derived

Key observations:

  • The slot indices for f and g are preserved across the hierarchy. Derived::f occupies the same slot as Base::f. This is what makes virtual dispatch O(1)O(1): the slot index is a compile-time constant.
  • Derived adds a new slot for h at the next available index.
  • The vtable also stores a pointer to std::type_info for RTTI support.

Let ff be a virtual function at slot index kk in the vtable. Let pp be a pointer to a polymorphic Object. The dispatch sequence is:

  1. Load the vptr: vptr = *(void**)p — one memory load at a fixed offset from the object.
  2. Load the function pointer: func = vptr[k] — one indexed load at a fixed offset from the vptr.
  3. Call: func(p) — one indirect call.

Since kk is a compile-time constant determined by the function declaration order in the class, and The vptr offset within the object is also a compile-time constant (0 in the Itanium ABI), both Memory accesses are at fixed, known offsets. The total work is two memory loads and one indirect Branch, independent of the depth or breadth of the inheritance hierarchy. Therefore, virtual Dispatch is O(1)O(1).

The following program demonstrates the vptr overhead and the layout of vtables across an inheritance Hierarchy:

#include <iostream>
struct NonVirtual {
int x;
int y;
};
struct EmptyVirtual {
virtual void f() {}
virtual ~EmptyVirtual() = default;
};
struct SingleInheritance : EmptyVirtual {
int z;
void f() override {}
};
struct MultipleBases {
virtual void g() {}
virtual ~MultipleBases() = default;
};
struct DiamondBase1 {
virtual void a() {}
virtual ~DiamondBase1() = default;
};
struct DiamondBase2 {
virtual void b() {}
virtual ~DiamondBase2() = default;
};
struct DiamondDerived : DiamondBase1, DiamondBase2 {
void a() override {}
void b() override {}
virtual void c() {}
};
int main() {
std::cout << "NonVirtual: " << sizeof(NonVirtual) << " bytes\n";
std::cout << "EmptyVirtual: " << sizeof(EmptyVirtual) << " bytes\n";
std::cout << "SingleInheritance: " << sizeof(SingleInheritance) << " bytes\n";
std::cout << "MultipleBases: " << sizeof(MultipleBases) << " bytes\n";
std::cout << "DiamondDerived: " << sizeof(DiamondDerived) << " bytes\n";
}

Typical output on a 64-bit platform (Itanium ABI, LP64):

NonVirtual: 8 bytes
EmptyVirtual: 8 bytes
SingleInheritance: 16 bytes
MultipleBases: 8 bytes
DiamondDerived: 16 bytes

Analysis:

  • NonVirtual: two int members = 2×4=82 \times 4 = 8 bytes. No vptr.
  • EmptyVirtual: one vptr (8 bytes). Even with no data members, a polymorphic class pays the vptr cost, but no additional padding is needed when alignment is already satisfied.
  • SingleInheritance: vptr (8) + int z (4) + padding (4) = 16 bytes. The derived class reuses the base’s vptr — no additional vptr is added for single inheritance.
  • MultipleBases: one vptr = 8 bytes.
  • DiamondDerived: two vptrs (one per polymorphic base) + padding = 16 bytes. Each base class subobject carries its own vptr.

Every virtual call involves:

StepCost
Load vptr from object1 memory access (may hit L1 cache)
Index into vtable1 arithmetic op + 1 memory access
Indirect call1 branch (may be mispredicted)
Total extra vs. Direct~2—5 cycles on modern hardware (cached)

The primary costs are indirection (preventing inlining) and branch misprediction (the CPU Cannot predict which function will be called at the indirect branch). In tight loops, this can be Measurable.

#include <chrono>
#include <iostream>
#include <vector>
struct Base {
virtual double compute(double x) const { return x * x; }
virtual ~Base() = default;
};
struct Derived : Base {
double compute(double x) const override { return x * x * x; }
};
struct NonVirtualBase {
double compute(double x) const { return x * x; }
};
struct NonVirtualDerived : NonVirtualBase {
double compute(double x) const { return x * x * x; }
};
template <typename T>
double benchmark(const std::vector<T*>& objects, int iterations) {
auto start = std::chrono::high_resolution_clock::now();
volatile double sink = 0;
for (int i = 0; i < iterations; ++i) {
for (auto* obj : objects) {
sink += obj->compute(1.0000001);
}
}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> elapsed = end - start;
return elapsed.count();
}
int main() {
constexpr int N = 1000;
constexpr int ITERS = 100000;
std::vector<Base*> virtual_objects;
std::vector<NonVirtualBase*> nonvirtual_objects;
virtual_objects.reserve(N);
nonvirtual_objects.reserve(N);
for (int i = 0; i < N; ++i) {
virtual_objects.push_back(new Derived{});
nonvirtual_objects.push_back(new NonVirtualDerived{});
}
double vt = benchmark(virtual_objects, ITERS);
double nvt = benchmark(nonvirtual_objects, ITERS);
std::cout << "Virtual dispatch: " << vt << " s\n";
std::cout << "Non-virtual call: " << nvt << " s\n";
std::cout << "Ratio: " << vt / nvt << "x\n";
for (auto* p : virtual_objects) delete p;
for (auto* p : nonvirtual_objects) delete p;
}

:::caution The actual performance difference depends heavily on compiler optimization levels, CPU Branch prediction accuracy, and whether the compiler can devirtualize the call (see Devirtualization). With -O2 or -O3Modern compilers may eliminate The virtual dispatch entirely if the dynamic type is provable. :::

The final specifier has two uses [N4950 S11.7.4]:

  1. On a class: prevents the class from being used as a base class.
  2. On a virtual member function: prevents the function from being overridden in a derived class.
#include <iostream>
struct Base {
virtual void f() const { std::cout << "Base::f\n"; }
};
struct Mid : Base {
void f() const final { std::cout << "Mid::f (final)\n"; }
};
struct Leaf final : Mid {
};
int main() {
Leaf obj;
obj.f();
}

The following would be ill-formed:

struct Derived : Leaf { };
// error: cannot derive from 'final' class 'Leaf'
struct Other : Mid {
void f() const override { }
// error: declaration of 'f' overrides a 'final' function
};

The override specifier [N4950 S11.7.3] instructs the compiler to verify that the function actually Overrides a virtual function from a base class. It catches accidental mismatches in signature or Constness:

#include <iostream>
struct Base {
virtual void process(int x) const { std::cout << "Base: " << x << "\n"; }
virtual ~Base() = default;
};
struct Correct : Base {
void process(int x) const override { std::cout << "Correct: " << x << "\n"; }
};
struct Wrong : Base {
void process(int x) { std::cout << "Wrong (non-const)\n"; }
// Missing 'const' -- this HIDES Base::process, does NOT override it.
// The compiler would NOT warn without 'override'.
// Adding 'override' here would produce an error.
};

:::tip Best Practice Always use override on every function intended to override a base-class Virtual function. This eliminates an entire class of bugs caused by signature mismatches. :::

1.7 Virtual Dispatch During Construction and Destruction

Section titled “1.7 Virtual Dispatch During Construction and Destruction”

A critical and often surprising rule: virtual calls from constructors and destructors do not Dispatch dynamically [N4950 S11.9.3]. During base class construction, the derived portion of the Object has not yet been constructed, so the vptr points to the base class’s vtable. Any virtual call Resolves to the base class’s version, even if the derived class overrides it.

By [N4950 S11.9.3], during the execution of a constructor for class B (where B has base class A), the following sequence occurs:

  1. Base class A’s constructor runs first. The vptr is set to A::vtable at the beginning of A’s constructor body. Any virtual call during A’s construction dispatches through A::vtable.
  2. After A’s constructor completes, control enters B’s constructor. The vptr is set to B::vtable. Virtual calls during B’s construction dispatch through B::vtable.
  3. During destruction, the process reverses: B’s destructor runs first (vptr = B::vtable), then A’s destructor runs (vptr = A::vtable).

This is necessary for correctness: calling Derived::do_work() before the Derived members are Initialized would access uninitialized memory, which is undefined behavior.

#include <iostream>
struct Base {
Base() {
std::cout << "Base ctor: ";
do_work();
}
virtual ~Base() {
std::cout << "Base dtor: ";
do_work();
}
virtual void do_work() { std::cout << "Base::do_work\n"; }
};
struct Derived : Base {
int data_{42};
Derived() {
std::cout << "Derived ctor: ";
do_work();
}
~Derived() override {
std::cout << "Derived dtor: ";
do_work();
}
void do_work() override {
std::cout << "Derived::do_work data_=" << data_ << "\n";
}
};
int main() {
Derived d;
std::cout << "---\n";
}

Output:

Base ctor: Base::do_work
Derived ctor: Derived::do_work data_=42
Derived dtor: Derived::do_work data_=42
Base dtor: Base::do_work

The vptr transitions through three states during Derived object construction:

  1. During Base construction: vptr points to Base::vtable -> do_work() calls Base::do_work
  2. During Derived construction: vptr points to Derived::vtable -> do_work() calls Derived::do_work
  3. During Derived destruction: vptr is reset to Base::vtable -> do_work() calls Base::do_work

:::caution Calling a pure virtual function from a constructor or destructor is undefined Behavior [N4950 S11.9.3]. The pure virtual function has no definition to dispatch to (or the Definition is not called). Some implementations call the pure virtual handler and terminate the Program. :::

The Non-Virtual Interface pattern makes all public member functions non-virtual and delegates to Private virtual functions. This provides a stable public API while allowing customization through Virtual dispatch:

#include <iostream>
#include <stdexcept>
struct StreamProcessor {
// Public non-virtual interface: invariant checking happens here
void process(const std::string& data) {
if (data.empty()) {
throw std::invalid_argument("empty data");
}
std::cout << "pre-processing: " << data.size() << " bytes\n";
do_process(data);
std::cout << "post-processing complete\n";
}
virtual ~StreamProcessor() = default;
private:
// Private virtual hook: derived classes override this
virtual void do_process(const std::string& data) = 0;
};
struct JsonProcessor : StreamProcessor {
private:
void do_process(const std::string& data) override {
std::cout << "JSON processing: " << data << "\n";
}
};
struct XmlProcessor : StreamProcessor {
private:
void do_process(const std::string& data) override {
std::cout << "XML processing: " << data << "\n";
}
};
int main() {
JsonProcessor jp;
XmlProcessor xp;
jp.process(R"({"key": "value"})");
xp.process("<root><item/></root>");
}

The NVI pattern ensures that:

  • Pre-conditions and post-conditions are checked in the non-virtual wrapper (cannot be bypassed by a derived class override).
  • The virtual function signature is a stable extension point.
  • Template Method is easier to implement since the non-virtual function controls the algorithm skeleton.

With multiple inheritance, each polymorphic base class contributes its own vptr to the derived Object. The Itanium ABI designates the first polymorphic base as the primary base; the derived Class shares the primary base’s vptr and appends its own virtual function slots.

#include <iostream>
#include <cstdio>
struct A {
virtual void fa() { std::cout << "A::fa\n"; }
virtual ~A() = default;
int a_val{};
};
struct B {
virtual void fb() { std::cout << "B::fb\n"; }
virtual ~B() = default;
int b_val{};
};
struct C : A, B {
void fa() override { std::cout << "C::fa\n"; }
void fb() override { std::cout << "C::fb\n"; }
virtual void fc() { std::cout << "C::fc\n"; }
int c_val{};
};

Memory layout under the Itanium ABI:

C object:
offset 0: vptr_A (shared between A and C)
offset 8: a_val
offset 12: b_val
offset 16: vptr_B (separate vptr for B subobject)
offset 24: c_val

When calling fb() through a B*The compiler generates a thunk — a small code stub that Adjusts the this pointer by the offset between the B subobject and the C complete object Before jumping to C::fb. This thunk is stored in B’s vtable within C’s vtable structure.

C::B::vtable for fb:
thunk: this -= 16; jmp C::fb

The thunk ensures that C::fb receives a pointer to the start of the C object (not the B Subobject), so that C::fb can access a_val``b_valAnd c_val correctly.

1.10 Virtual Inheritance and vtable Complications

Section titled “1.10 Virtual Inheritance and vtable Complications”

Virtual inheritance introduces a virtual base pointer (vbptr) in addition to the vptr. The Location of the virtual base subobject is not known at compile time — on the Most-derived class. The Itanium ABI stores the virtual base offset in the vtable, adding another Level of indirection.

#include <iostream>
struct VBase {
virtual void vf() { std::cout << "VBase::vf\n"; }
virtual ~VBase() = default;
int vb_data{};
};
struct Left : virtual VBase {
virtual void lf() { std::cout << "Left::lf\n"; }
int l_data{};
};
struct Right : virtual VBase {
virtual void rf() { std::cout << "Right::rf\n"; }
int r_data{};
};
struct MostDerived : Left, Right {
void lf() override { std::cout << "MostDerived::lf\n"; }
void rf() override { std::cout << "MostDerived::rf\n"; }
int md_data{};
};

The layout is complex: each of Left and Right contains a vbptr that points to a shared subtable In the vtable. At runtime, these offsets are used to locate the single VBase subobject. Every Access to a virtual base member requires one additional indirection compared to non-virtual base Access.

1. Forgetting override: Without overrideA misspelled function name or wrong parameter type Silently creates a new function that hides (not overrides) the base version. Always use override on every function intended to be virtual dispatch.

2. Virtual functions in constructors/destructors: As shown in section 1.7, virtual dispatch does Not work as expected during construction and destruction. Never call virtual functions that rely on Derived-class state from a base class constructor or destructor.

3. Diamond inheritance without virtual: If two intermediate classes both inherit from the same Base non-virtually, the most-derived class contains two copies of the base. This causes ambiguity When calling base-class functions. Use virtual inheritance when a diamond is intended, or Restructure the hierarchy.

4. Pure virtual function with body: A pure virtual function (= 0) can have a definition. The class is still abstract, but derived classes can call Base::f() explicitly. This is useful for Providing shared default behavior that derived classes can opt into:

struct Base {
virtual void f() = 0;
};
void Base::f() { std::cout << "Base::f default\n"; }
struct Derived : Base {
void f() override { Base::f(); std::cout << "Derived::f\n"; }
};

5. Overhead of virtual dispatch in tight loops: While the per-call overhead is small (~2—5 Cycles), the inability to inline virtual calls prevents a wide range of optimizations. If a function Is called in a tight inner loop and the target is always the same, consider devirtualization (mark The class or function final) or CRTP/deducing-this for static dispatch.

6. Virtual functions and move semantics: Virtual functions cannot be templated. If you need a Type-parameterized operation that is dispatched at runtime, you must use a type-erased mechanism (e.g., std::functionA virtual wrapper) rather than a virtual template.

7. Virtual function calls and constexpr: Virtual function calls are not allowed in constant Expressions [N4950 S7.7]. A constexpr function cannot contain a virtual call because the target is Determined at runtime. If you need compile-time dispatch, use if constexpr with type traits, CRTP, Or deducing this.

8. Calling virtual functions through destructors of base classes: When a derived class Destructor completes and the base class destructor begins, the vptr is reset to the base class’s Vtable. Any virtual calls during the base destructor dispatch to the base version, not the derived Version. This is the reverse of the construction order.

1.12 Virtual Functions and override vs final Interaction

Section titled “1.12 Virtual Functions and override vs final Interaction”

override and final are independent specifiers that can be combined:

  • void f() override; — verifies that f overrides a base class virtual function.
  • void f() final; — prevents further overriding (implies this function overrides a base).
  • void f() override final; — both: verifies the override and prevents further overriding.
  • void f() final override; — same as above (order does not matter).
#include <iostream>
struct Base {
virtual void f() { std::cout << "Base\n"; }
virtual ~Base() = default;
};
struct Mid : Base {
void f() override final { std::cout << "Mid\n"; }
};
// struct Bad : Mid {
// void f() override {} // ERROR: f is final
// };
int main() {
Mid m;
m.f(); // Mid
}

1.13 Virtual Functions and Object Size Across Platforms

Section titled “1.13 Virtual Functions and Object Size Across Platforms”

The size of a vptr depends on the platform’s pointer size:

PlatformPointer Sizevptr SizeEmpty Polymorphic Class Size
32-bit x864 bytes4 bytes4 bytes
64-bit x86-648 bytes8 bytes8 bytes
64-bit ARM8 bytes8 bytes8 bytes
32-bit ARM4 bytes4 bytes4 bytes

On 64-bit platforms, every polymorphic object pays at least 8 bytes for the vptr, even if the class Has no data members. This is the fundamental cost of runtime polymorphism.

This topic covers the mathematical techniques and concepts related to virtual functions and vtables, including key theorems, methods, and problem-solving approaches.

Key concepts include:

  • arithmetic and geometric sequences
  • series and sigma notation
  • recurrence relations
  • convergence tests
  • mathematical induction

Regular practice with a variety of question types is essential to build fluency and confidence in applying these mathematical techniques.

Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.