DEV Community

Pierre Gradot
Pierre Gradot

Posted on • Updated on

vtables under the surface | Episode 1 - Concepts

If you've been around C++ for a while, you've likely come across the terms "vtable", "virtual table" or "virtual method table". Vtables are not part of the C++ standard, even though this concept pops up almost immediately when you try to understand how virtual functions actually work in C++. Indeed, vtables are the most common implementation of polymorphism in C++.

You may also have already encountered a cryptic compilation error like "undefined reference to vtable for MyClass". This may have left you perplexed because you didn't explicitly create anything named "vtable" in your code. This error signals that, under the hood, the compiler generates vtables to handle virtual functions. In you're curious about this error, here is a good discussion on stackoverflow (the second answer is particularly enlightening).

In this series, we're going to delve into the depths and explore how a vtable works at the byte and assembly levels.


As mentioned in the introduction, we'll be discussing implementation details. By definition, these details may vary from one compiler to another or from one OS to another. They may even change in the next version of the same compiler for the same OS.

However, most major compilers (except MSVC) follow the Itanium C++ ABI:

The Itanium C++ ABI is an ABI for C++. As an ABI, it gives precise rules for implementing the language, ensuring that separately-compiled parts of a program can successfully interoperate. Although it was initially developed for the Itanium architecture, it is not platform-specific and can be layered portably on top of an arbitrary C ABI. Accordingly, it is used as the standard C++ ABI for many major operating systems on all major architectures, and is implemented in many major C++ compilers, including GCC and Clang.

The ABI has a section about vtables, so using any compiler following this ABI should yield similar implementation details.

In this series, I will use GCC 12.2.0 on Debian BookWorm (unless stated otherwise).

The Code

Here is a code that will serve as the foundation for the discussion in the series (at least for the first episodes). It is split into 3 different files:


#pragma once

struct Base {
    virtual void foo() const;
    virtual void bar() const;
    int dummy_base;

struct Derived : Base {
    void foo() const override;
    void bar() const override;
    int dummy_derived;

void use(const Base&);
Enter fullscreen mode Exit fullscreen mode


#include "code.hpp"

#include <cstdio>

void Base::foo() const {
    std::puts("Base => foo()");

void Base::bar() const {
    std::puts("Base => bar()");

void Derived::foo() const {
    std::puts("Derived => foo()");

void Derived::bar() const {
    std::puts("Derived => bar()");

void use(const Base& obj) {;
Enter fullscreen mode Exit fullscreen mode


#include "code.hpp"

int main() {
    auto obj = Derived();
Enter fullscreen mode Exit fullscreen mode

This code is a typical example of polymorphism using virtual functions. use() receives a reference to a Base object. At runtime, dynamic dispatch selects and calls Derived::foo() and the program prints Derived => foo(). Looking at this code, you don't see any vtables. We will analyze the generated binary and find them later in the series.

Build and Run

You can easily build the program with CMake:

cmake_minimum_required(VERSION 3.25)


target_compile_options(a.out PRIVATE
Enter fullscreen mode Exit fullscreen mode

From the root directory of the project:

$ cmake -B build -S .
$ cmake --build build
$ ./build/a.out 
Derived => foo()
Enter fullscreen mode Exit fullscreen mode


  • Separated source files avoid possible compiler optimizations.
  • -O1 generates an assembly code that is much easier to read and understand than -O0.

We're Ready

We are now ready. Let's dive into vtables!

I will publish the episodes as I write them. Subscribe to be notified! 🎉

Top comments (0)