C++ Virtual Functions and VTables
Learn how C++ virtual dispatch works under the hood: vtables, vptrs, override, final, and the cost of polymorphism.
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 |
+---------+ +------------------+ 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
finalto enable devirtualization optimizations. - Prefer composition or
std::variant+std::visitwhen you have a small, closed set of types. Faster and clearer. - Use
dynamic_castonly 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.
Related articles
- C++ C++ Move Semantics and Rvalue References
How move semantics work in modern C++: rvalue references, std::move, perfect forwarding, and the rules that decide when your objects are copied versus moved.
- C++ C++ std::string_view Explained
Learn how std::string_view gives you cheap, non-owning views into strings — when to use it, how it speeds up code, and how to avoid dangling references.
- C++ C++ CMake Tutorial: Build Your First Project
Learn modern CMake for C++ — targets, properties, and dependencies — and configure a small project that compiles cleanly across platforms.
- C++ C++ constexpr and Compile-Time Computing
How constexpr, consteval, and constinit let you move computation from runtime to compile time, with practical patterns and the rules that govern them.