Virtual tables and dynamic dispatch #
1 - Introduction #
This article is an attempt to explain how virtual tables work. I will be following the Itanium C++ ABI, which is what most vendors are based on, with the exception of none other than Microsoft, with MSVC.
Why do we even need virtual tables? The answer is dynamic dispatch. Which can be defined as: the process of selecting which implementation of a polymorphic operation to call at runtime.
Before getting into this. Previous to this article I wrote another one titled C++ class padding and memory layout. I will try to make this one agnostic, so you are not forced to read it, but, things will make much more sense if you do so.
2 - Simple inheritance #
In this snippet of code, which foo() gets called? And more importantly, how?
class A {
public:
virtual void foo() {}
};
class B : public A {
public:
void foo() override {}
};
int main(){
B b;
A* a = &b;
a->foo();
}
If we take the code example from above, class A will have a virtual table, since it declares a virtual function. Using the following command we can see what it looks like.
g++ -fdump-lang-class -c main.cpp

There are two separate structures in the output. One is obviously the virtual table of class A, the other one is the memory layout, how data members (including the vptr), are stored. For a better view on it, again I highly encourage you to read this.
Notice that class A’s size is 8 bytes, since it only has the virtual pointer. It points to the virtual table of A with an offset of 16 bytes, where the pointer to function foo() is stored.
- (0) Offset-to-top: distance to the complete object, this makes sense when a class is derived from multiple base classes. We will come back to it.
- (8) Typeinfo pointer: this is a pointer to a structure that contains information about the class. This enables RTTI (Run-Time Type Information), getting the type of an object at runtime. Used for casting for example.
Continuing with B. Class B will also have a virtual table, because it inherits a virtual function, foo(), that overrides.

When there is inheritance, we need to separate what came from the parent class, from what is new (in terms of virtual functions). This means that usually a class x, would have as many vptrs, as different classes, that class x is composed of. If there is a derived class, that inherits from 3 base classes. You would have the 3 vptrs from the parents, plus a new one for that child class. This is a half-truth, because of order rules in inheritance, there is a way to save space by reusing one of the parent’s vptr for the child.
Let’s see this with our example.
When we create an object B, in B’s virtual table there are 2 spaces to maintain, A (parent) and B (itself). We first inherit a vptr from A, that vptr is now assigned to B’s virtual table. Not just to any point of the virtual table, the offset has to be to A’s first virtual function, in this case it’s easy since there is only one, foo() (remember the concept of the address point). That would be the space for A.
For B: The child class inherits all the functions, so its first function/address point, is going to be the same one as the primary base class. In this case, B’s vptr is also going to point to foo(). Even if B declared another virtual function, lets say baz(), B’s address point would still be foo(), since it’s the first function of the first parent class. That is why in the memory layout, you only see the vptr A, renamed to B, instead of two separated vptrs.
3 - Double inheritance #
Moving to a more complete example:
class A {
public:
int data_A;
virtual void foo() {}
};
class B {
public:
int data_B;
virtual void goo() {}
};
class C : public A, public B {
public:
int data_C;
void foo() override {}
virtual void baz() {}
};
Now we have two virtual pointers, in reality they point to the same virtual table (C’s), just with a different offset. I want to clarify that each class always has its own virtual table. When in the explanations you see vptr_B, is one thing to have that in the context of class B (that vptr_B will point to B’s virtual table), or to have vptr_B in the context of a class C (now the vptr_B inherited by C, will point to C’s virtual table with maybe an offset).

I hope you already understand why we only need two vptrs, despite being 2 parents and a child class. In any case lets go again:
- vptr_A needs to point to foo(), offset 16.
- vptr_B needs to point to goo(), offset 48.
- Now, the derived class, has access to all of them, because of inheritance. So, it needs to point to the first one, and because of inheritante order that will be foo(). We can use vptr_A also for C without the need of creating another one, since they share the same offset. The vptr does appear as C’s, because it gets renamed. But, if you look in the memory layout, in reality it’s the one inherited from A.
Be also aware that baz() has its entry in the virtual table after foo(), but, before goo(). The order in the vtable is the following:
- Primary base
- Derived class (including overrides)
- Rest of base classes …
4 - Adjusting pointers #
When are virtual tables used?
When we create an object of type C, and make a call to a polymorphic operation, which can be in this case foo(), the program doesn’t need to check the virtual table, because objects are always of that type, they have a fixed structure and size.
This changes with pointers, because we can have a pointer of a base class point to different children. Imagine a buffer where different messages can go through, for dealing with them you will need to use a Message*, and depending on what comes through it might point to a Commit or an Update child class. Or if Message is not abstract, it could actually point to a plain Message.
That’s what virtual tables are for. With that Message* you can execute message->multicast(), and despite not knowing the underlying type, it will make the correct call, considering there are multiple implementations.
Let’s now tweak a little bit the code and make C override goo() instead of foo().
class A {
public:
int data_A;
virtual void foo() {}
};
class B {
public:
int data_B;
virtual void goo() {}
};
class C : public A, public B {
public:
int data_C;
void goo() override {}
virtual void baz() {}
};
This produces the following virtual table:

Pretty similar to what we could expect, with the exception that now vptr_B doesn’t point to C::goo offset 24, is pointing to a weird function C::_ZThn…. That is a thunk, we will talk about it in a minute.
To understand it better, consider this layout for an object C. Color blue for A’s part, green for B’s and in red you have the new addition. But, keep in mind, that the C object is all of it, not just the part in red. The dash represents padding. Total size: 32 bytes, as indicated above.
(vptrA will probably appear renamed as vptrC)
As we all know when you call a member function there is a parameter that is passed automatically, the this pointer, that points to the instance. A member function of class A, will expect this to be of type A*. Taking the example of function foo(), that is inherited from A. Even if we call it from C, foo() still expects this to be of type A*.
// Imagine the previous A, B and C classes
int main(){
A a;
a.foo(); // ok, everything works
C c;
c.foo(); // this is a valid call, but how does it work?
};
The compiler will need to consider if conversion is needed.
If you look again at the memory layout of C, you can see that an object C actually starts with A’s vptr and int, followed by the rest of the things that make up class C. This means that both objects A and C start at the same point, with the difference that C continues and adds more things.

So there is actually no conversion needed, foo() will receive a this pointer in the correct position and since it will treat it as one to A, it won’t surpass the limit where B’s data starts.

Even the vptrA, which in reality has been hijacked by child C, still points to the correct position in the virtual table. Despite not being used now.
We can take a look at the assembly code, with x86-64 gcc 16.1:
(In this case, “Reserve memory” means extending the stack, not the heap)
The process is exactly the same with C, because there is no conversion needed. And as you can see, no virtual tables in sight, since we are dealing with plain objects.
This would be the assembly if we had used a pointer:

The same would happen if we used a reference. Now, if some conversion was needed the compiler would need to adjust the pointer.
This pointer adjustment will be done easily by the compiler if we are working with objects. But, not with pointers.
Let’s go back to the Message example. If you receive a new Message to the buffer, you need to treat it with a pointer if you want to conserve polymorphism. The catch is that the compiler doesn’t know the object is working with, underneath that pointer, there could be an object of type Message, Commit, Update… whatever.
Since it’s not able to resolve the underlying object, if you make a call to a member function, it will delegate the call to the virtual table.
The virtual table is now in charge of:
- Adjusting the pointer.
- Calling the implementation.
In the following code, we create an object C, and point to it with a B* (this can be extrapolated to the Message example). When doing this, the compiler automatically adjusts by 16 bytes, now b points directly to where the B subobject lives within C.
From this point on, we just have B*’s perspective, which doesn’t know what is the real type of the object underneath. It could be of type B, and needing to call B::goo(). Or, of type C, being the correct call to C::goo(). Apart from needing to adjust the pointer.
This is delegated to the virtual table, via the thunk. Remember from the virtual table that vptr_B instead of pointing to C::goo(), it pointed to a strange function. That is C::goo()’s thunk, as you can see in the following assembly code, that thunk acts as a wrapper, first adjusting the pointer, and then making the call.

To clarify, when b takes as value, c’s memory address, the compiler will add 16 bytes. Pointing b know, to b’s subobject inside c.
Same would happen if B* pointed to a B object. The thing is that after dereferencing the vptr, instead of a thunk, you would have the direct call to B::goo().
5 - More thunk examples #
If function foo() wasn’t overridden by C, the thunk wouldn’t be generated. Because there is no conversion needed.
class A {
public:
int data_A;
virtual void foo() {}
};
class B {
public:
int data_B;
virtual void goo() {}
};
class C : public A, public B {
public:
int data_C;
virtual void baz() {}
};
int main(){
C c;
B* b = &c;
b->goo();
}

vptr_B points to offset 48, B::goo(), no thunk generated. Another example of how the call is delegated to the vtable, we can see how it dereferences the vptr, finding B::goo().
An example with multiple thunks looks like this:
class A {
public:
int data_A;
virtual void foo() {}
};
class B {
public:
int data_B;
virtual void goo() {}
virtual void hoo() {}
};
class C : public A, public B {
public:
int data_C;
virtual void baz() {}
void goo() override {}
void hoo() override {}
};

As you can see two thunks have been generated. One for C::goo() and the other one for C::hoo(). Thunks are unique to the object-function, that’s why there is one, per virtual function of B, overridden by C.
For the same reason that when there is no conversion needed, no thunk is generated, like when the function is not overridden. Thunks are not created for the primary base, since as we’ve already seen, the this pointer is already adjusted.
class A {
public:
int data_A;
virtual void foo() {}
virtual void koo() {}
};
class B {
public:
int data_B;
virtual void goo() {}
virtual void hoo() {}
};
class C : public A, public B {
public:
int data_C;
virtual void baz() {}
void foo() override {}
void koo() override {}
void goo() override {}
void hoo() override {}
};

No thunks are created for C::foo() and C::koo(), because the layout has the same start for classes A and C.
6 - Object slicing #
There is an interesting case, for previous examples we have been creating an object, taking its memory address and assigning it to a pointer, but, what about the opposite?
int main(){
C* c = new C; // Pointer to object C
A a = *c; // Create A by dereferencing c
a.foo();
}
Unlike with pointers, this won’t work as expected. We are not making a’s memory address the same as c’s, we are creating a new object via the copy constructor.
This is called object slicing, creating a new object A, instead of pointing to c. It will just maintain the value of some data members, such as c’s value of data_A. The rest will be “sliced”. Same happens with the virtual table, the vptr will point to vtable A, not C’s.

7 - Offset-to-top #
One of the things that has been overlooked, are conversions such as this:
C c;
B* b = &c;
This one is done very easily by the compiler, because it knows both structures.

But, we did mention earlier the offset-to-top, a value in the virtual table that indicates to every vptr how far they are from the base object, and we still haven’t found a use case for it.
The offset-to-top is a value put in the virtual table, such placement might seem on purpose to be used by thunks. But actually, thunks don’t use them, they already know beforehand the adjustment needed, since each of them is tailored for a specific virtual member function.
So, when is this field used?
For conversions, such as dynamic_cast. Going back to the Message buffer example, if at some point you want to know what is the underlying type, you could do it with something like this:
bool is_Commit(Message* message){
if( Commit* c = dynamic_cast<Commit*>( message ); c != nullptr ){
return true;
}
return false;
}
Here, dynamic_cast will make use of the offset-to-top value for adjusting the pointer. Returning true, if the cast is successful.
8 - Conclusions #
Initially, there was another section in the article. Related to the diamond problem, since it adds some more structures and complexity to the dynamic dispatch. But I think this is already long enough, so maybe I will make my next article about that.