25 / 163 · C++11 · 9 min
Vtables: Language Semantics and ABI Implementation
The standard specifies which final overrider a virtual call should select, but it does not specify the number of vtables, the location of a vptr, or the object's memory map. Common ABIs implement those semantics with vtables, adjustment thunks, and RTTI metadata. Understanding them helps debugging, but you must not verify them with unguaranteed memory reads.
In this lesson
State the behavior the standard guarantees first
When a virtual function is called through a base-class pointer or reference, the final overrider corresponding to the actual object is usually executed. Explicitly qualified calls and the construction and destruction phases have extra rules. The standard requires this observable behavior. It does not require that the object header contain a pointer called a vptr, and it does not require every implementation to use the same vtable layout.
Therefore “one virtual function always adds one pointer's worth of size to the object” is only a rule of thumb for simple cases under a particular ABI. Alignment, reuse of a primary-base layout, multiple inheritance, and virtual inheritance all change layout. A compiler may even eliminate a virtual call or the object itself, provided observable behavior is unchanged.
How a common ABI supports multiple inheritance
In the Itanium C++ ABI, for example, a vtable may hold not only function entries but also offsets to the complete object, type information, and virtual-base location data. Different base subobjects in a multiple-inheritance hierarchy may need different vtable views. A call entry may also go through a thunk that adjusts this before entering the derived implementation.
Those details explain why starting from different base pointers can still correctly reach the derived object. They are not a public interface for ordinary programs to manipulate by hand, and you must not assume that every vtable slot is an address that can be called as an ordinary function pointer. Study layout with the compiler's layout dump, a debugger, and the ABI document for the target platform.
Verify behavior; do not probe unguaranteed bytes
The example calls the implementation of one complete object through two base interfaces, and it uses a well-formed dynamic_cast for a cross-cast, verifying standard-level behavior. It does not read an integer at the start of the object, and it does not cast the object address to a multi-level pointer and dereference it. It therefore does not depend on layout, aliasing rules, or a coincidental result from one compiler.
When the compiler can prove the dynamic type, it may devirtualize and inline the call. final can sometimes help provide that proof, but it does not guarantee that any particular assembly instruction disappears. Performance judgments should rest on a real optimized build and on actual data distribution, not on charging a fixed cost merely because the source text contains virtual.
Pitfalls
- reinterpret_cast of an object pointer to a vtable pointer followed by a read is not a standard-blessed general reflection mechanism. An experiment that appears to work does not prove portability.
- One vtable per class and one vptr per object are not universally true layout rules, especially with multiple and virtual inheritance.
Run an example
Minimum C++11 · complete program · Download .cpp
#include <cassert>
struct Reader {
virtual int read() const = 0;
virtual ~Reader() = default;
};
struct Writer {
virtual void write(int value) = 0;
virtual ~Writer() = default;
};
class Cell final : public Reader, public Writer {
int value_ = 3;
public:
int read() const override { return value_; }
void write(int value) override { value_ = value; }
};
int main() {
Cell cell;
Reader* reader = &cell;
Writer* writer = &cell;
assert(reader->read() == 3);
writer->write(8);
assert(reader->read() == 8);
assert(dynamic_cast<Writer*>(reader) == writer);
assert(dynamic_cast<void*>(reader) == static_cast<void*>(&cell));
}
Compile locally
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread objects-vtable.cpp -o example && ./exampleExpected result
Expected: exit 0, no output; every assert holds.
CHECK YOUR UNDERSTANDING
Close the answer. Explain it.
Can you verify that there are two interfaces here by asserting sizeof(Cell) == 2 * sizeof(void*) + sizeof(int)?
Show a reference answer
No. Interface behavior is guaranteed by the language rules; sizeof also depends on alignment, padding, and a concrete ABI layout. The given equality may even fail on a common platform because of trailing padding. Keep the behavioral assertions that read and write through both interfaces and that perform the cross-cast. If you study size, record a measurement on the target platform and make clear that it is not a cross-platform contract.
Check the sources
Drafts and official chapters change. The version mark is only the example’s minimum.