Skip to content
C Codeloom
C++

C++ Virtual Functions and VTables

Learn how C++ virtual dispatch works under the hood: vtables, vptrs, override, final, and the cost of polymorphism.

·3 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • Virtual dispatch mechanics
  • VTable and vptr layout
  • override and final
  • Pure virtual functions
  • Devirtualization

Prerequisites

  • Basic familiarity with C++ classes

What and Why

When you call a non-virtual function, the compiler knows which code to run at compile time. When you call a virtual function, the compiler defers the decision to runtime, choosing the implementation based on the dynamic type of the object. This is the heart of polymorphism in C++.

The cost is a small indirection through a per-class table of function pointers (the vtable) accessed via a hidden pointer in each object (the vptr).

Mental Model

Each class with at least one virtual function has a vtable: an array of function pointers, one per virtual method, populated by the compiler. Each instance of such a class carries a hidden vptr that points to its class’s vtable. Calling a virtual function becomes “load vptr, index into vtable, call”.

Dog object         Dog vtable
+---------+        +------------------+
| vptr ---|------> | Animal::~Animal  |
+---------+        | Dog::speak       |
| name    |        | Dog::run         |
+---------+        +------------------+
Object layout with vptr and vtable

Hands-on Example

#include <iostream>
#include <memory>
#include <vector>

class Animal {
public:
  virtual ~Animal() = default;
  virtual void speak() const = 0; // pure virtual
};

class Dog : public Animal {
public:
  void speak() const override { std::cout << "Woof\n"; }
};

class Cat final : public Animal {
public:
  void speak() const override { std::cout << "Meow\n"; }
};

int main() {
  std::vector<std::unique_ptr<Animal>> zoo;
  zoo.push_back(std::make_unique<Dog>());
  zoo.push_back(std::make_unique<Cat>());
  for (const auto& a : zoo) a->speak(); // virtual dispatch
}

Note three keywords: virtual declares dispatch, override makes the compiler verify you’re really overriding, and final prevents further overriding (and helps devirtualization).

Common Pitfalls

Missing virtual destructor. Deleting a derived object through a base pointer with a non-virtual destructor is undefined behavior. If a class has any virtual function, give it a virtual destructor (even if it’s = default).

Forgetting override. Without it, a typo in the signature creates a brand new function that hides nothing and never gets called via the base pointer. Always write override.

Calling virtuals in constructors/destructors. Inside Base::Base, the object’s dynamic type is still Base. Virtual calls dispatch to Base’s version, not the derived one. Most likely not what you want.

Slicing. Assigning a derived object to a base value (not pointer or reference) copies only the base subobject. The derived parts are sliced off and virtual behavior is lost.

Performance assumptions. Virtual calls add a single indirection plus an unpredictable branch. Usually negligible. But in inner loops over small objects, that overhead plus disabled inlining is measurable.

Practical Tips

  • Mark leaf classes final to enable devirtualization optimizations.
  • Prefer composition or std::variant+std::visit when you have a small, closed set of types. Faster and clearer.
  • Use dynamic_cast only when you genuinely need runtime type identification; reach for it sparingly.
  • The Curiously Recurring Template Pattern (CRTP) gives static polymorphism without vtables when you can resolve types at compile time.
  • Inlining cannot cross a virtual call. Hot paths that need inlining should not be virtual.

Wrap-up

Virtual functions trade a tiny amount of runtime cost for the flexibility of late binding. Understanding the vtable model demystifies the language: you can predict object sizes, reason about ABI compatibility, and decide when polymorphism is worth it. Use override, give every polymorphic base a virtual destructor, and reach for final or compile-time alternatives when performance demands.