Most C++ developers can describe what virtual functions do. Far fewer can describe what they actually are, in memory, at the bit level. The mechanism is simple once you see it, and seeing it changes how you think about inheritance, polymorphism, and the hidden costs of both.
This is a tour of the layout. We'll start with a plain struct, add a virtual function, derive from it, then break things with multiple inheritance and put them back together with virtual inheritance. By the end you'll have a clear mental model of what your objects look like when the compiler is done with them.
The simple case: no virtuals, no surprises
A struct with no virtual functions has the layout you'd expect from reading the source. Members are placed sequentially in memory, with padding inserted to satisfy alignment requirements.
struct Simple {
int x; // offset 0
double y; // offset 8 (padded for alignment)
};
// sizeof(Simple) == 16 on a typical 64-bit system
No hidden fields. What you declared is what you got, plus a bit of padding. This is how every non-polymorphic class behaves in C++. It's also the layout you'd get in C.
What changes the moment you add virtual
Add a single virtual function to a class and the compiler injects a hidden pointer into every instance. That pointer is called the vptr, and it points to a per-class table of function pointers called the vtable.
class Base {
public:
int x;
virtual void foo();
virtual void bar();
};
Two things to internalize from this picture. First, the vptr is typically placed first, before your declared members. Second, there's exactly one vtable per class, not per object. The vtable is a static thing the compiler generates at compile time. Every instance of Base just stores a pointer to the same shared table.
So the per-object overhead is just one pointer: 8 bytes on a 64-bit system. The vtable itself doesn't grow with the number of instances. Make a million Bases and you have a million vptrs all pointing to the same place.
Inheritance: a new vtable, mostly the same shape
When you derive from a polymorphic class and override some functions, the derived class gets its own vtable. Overridden slots point to the new implementations. Inherited-but-not-overridden slots still point to the base's implementations.
class Derived : public Base {
public:
int y;
void foo() override; // overrides Base::foo
virtual void baz(); // new virtual, appended
};
When you call basePtr->foo(), the runtime does exactly three things:
Read the vptr. Load the hidden first field at basePtr + 0. This is one memory access.
Index into the vtable. Slot 0 holds foo. Load the function pointer at vptr + slot * 8.
Call it. An indirect call through the function pointer just loaded.
If the actual object is a Derived, the vptr points to Derived's vtable, so slot 0 resolves to &Derived::foo. That's it. That's the entire mechanism of runtime polymorphism in C++. There's no magic — it's just one extra load and one indirect call compared to a direct function call.
The vtable lookup itself is two pointer dereferences and a call. Modern CPUs predict indirect branches well when the call site is monomorphic (always hitting the same target). The actual cost of a virtual call in tight code is often indistinguishable from a direct call until you start mixing types in a way that defeats branch prediction.
Multiple inheritance: now you have two vptrs
When a class inherits from two unrelated polymorphic bases, each base gets its own subobject inside the derived class, and each subobject has its own vptr.
class A {
public:
int a;
virtual void fa();
};
class B {
public:
int b;
virtual void fb();
};
class C : public A, public B {
public:
int c;
void fa() override;
void fb() override;
virtual void fc();
};
Now things get interesting. When you hold a B* that actually points to a C object, that pointer does not point to offset 0 of the C. It points to offset 16, which is the start of the B subobject. The compiler adjusts the pointer value during the cast.
This is why casting between base classes in multiple inheritance isn't always a no-op. Going from C* to B* is an addition. Going from B* back to C* is a subtraction. The compiler tracks the offsets and inserts the arithmetic for you.
The thunk problem
Here's where it gets subtle. When someone calls fb() through a B*, the runtime reads vptr_B, looks up fb's slot, and finds &C::fb. But C::fb is a member function of C. It expects this to point to the full C object at offset 0, not the B subobject at offset 16.
The vtable entry doesn't actually point directly to C::fb. It points to a small compiler-generated stub called a thunk, which subtracts 16 from this, then jumps to the real C::fb. The cost is a few extra instructions, invisible at the source level, but real in the generated code.
Multiple inheritance gives you two vptrs, pointer arithmetic on every cast, and thunks on virtual calls. It works, but it's not free. This is one reason it has a reputation.
The diamond problem
Things get worse when the inheritance graph forms a diamond. Suppose you have an Animal base and two intermediate classes, Mammal and WingedAnimal, that both inherit from Animal. Now you want a Bat that inherits from both.
class Animal {
public:
int weight;
virtual void speak();
};
class Mammal : public Animal { int fur_length; };
class WingedAnimal : public Animal { int wingspan; };
class Bat : public Mammal, public WingedAnimal {
void speak() override;
};
Without doing anything special, a Bat contains two separate copies of Animal: one inside Mammal, one inside WingedAnimal. Each has its own weight field at a different memory location.
Now bat.weight is ambiguous. Which weight? The one inside Mammal or the one inside WingedAnimal? You have to write bat.Mammal::weight or bat.WingedAnimal::weight, and they refer to different memory locations holding different values. This is the diamond problem.
Virtual inheritance, and what it costs
The fix is to mark the inheritance as virtual, which tells the compiler "no matter how many paths reach Animal, only one copy should exist."
class Mammal : virtual public Animal { /* ... */ };
class WingedAnimal : virtual public Animal { /* ... */ };
class Bat : public Mammal, public WingedAnimal { /* ... */ };
Now there's one shared Animal subobject. But this completely changes the memory layout. Mammal and WingedAnimal can no longer assume where the Animal subobject lives relative to themselves, because that offset depends on the most-derived type.
The critical mechanism is that each vtable now stores a virtual base offset (sometimes called vbase_offset). When code inside Mammal needs to access the Animal subobject, it can't just walk to a known offset. It reads the offset from its vtable, then uses that to find Animal.
In a standalone Mammal, the Animal subobject might be at offset 16. In a Bat, the same Mammal code needs to find Animal at offset 32. The offset in the vtable adapts per most-derived class. This is an extra indirection on every access to the virtual base, on top of the normal virtual function dispatch.
And constructors get weird too
Virtual bases are constructed first, by the most-derived class. So Bat's constructor calls Animal's constructor directly. Mammal's and WingedAnimal's constructors skip the Animal initialization when they're being constructed as part of a Bat, because Bat already handled it. This is why virtual base constructors must be explicitly invoked in the most-derived class's initializer list if they take arguments.
The cost summary
It's worth laying out the costs explicitly, because the difference between the simple case and the complicated cases is dramatic.
| Feature | Memory per object | Cost per virtual call |
|---|---|---|
| Plain struct (no virtuals) | Just the declared members | N/A (direct calls only) |
| Single inheritance, virtual functions | +1 pointer (the vptr) | One indirect call through vtable |
| Multiple inheritance (N polymorphic bases) | +N pointers (one vptr per base subobject) | Indirect call, plus a thunk if this needs adjustment |
| Virtual inheritance | +N pointers, virtual base at variable offset | Indirect call, plus vbase offset lookup for base access |
This is also why "always prefer composition over inheritance" advice exists. Composition costs nothing structural. Inheritance, especially when it gets exotic, costs real bytes per object and real cycles per call.
Why this matters for type erasure
This connects directly to the type erasure pattern. When you build a type-erased wrapper around a polymorphic Concept base class, the std::unique_ptr<Concept> inside your wrapper is leveraging exactly this vtable machinery. The Concept base class has virtual functions. The templated Model<T> overrides them. The vptr inside the heap-allocated Model<T> is what makes the indirect call dispatch to the right implementation at runtime.
Type erasure with value semantics is essentially a clever wrapper around the same vptr-and-vtable dance you've been looking at here. The "manual vtable" form of type erasure I wrote about previously is even more direct — instead of relying on the compiler to build the vtable for you, you build it by hand with function pointers stored as members. Same mechanism, different presentation.
Everything about C++ polymorphism that feels mysterious resolves into a pointer, a table, and an indirect call. The compiler is doing the work. Once you can see the layout, you can predict the cost — and that's most of what separates "uses inheritance" from "uses inheritance well." Thanks for reading ✦
Comments