Adding a single virtual function to a class quietly changes the shape of every object it creates. The class gets larger by the size of one pointer, gains a hidden data member that the language never lets you name, and becomes a trivial-layout class no longer — with knock-on effects on memcpy validity, struct compatibility with C, and even how the compiler emits constructors.
The pointer in question is the vptr, and understanding what it is, where it lives, and when it is written is the foundation that makes everything else about virtual dispatch comprehensible.
Three things to take away:
- Every object of a class with at least one virtual function carries a hidden pointer — the vptr — to its class’s vtable.
- The vptr is written by the constructor, and rewritten as control passes through each level of an inheritance hierarchy.
- The vptr changes the class’s layout: it adds size, sits at a defined offset, and prevents the class from being a trivial type.
What a vptr is
The standard reference describes the mechanism plainly: each class with at least one virtual function has a hidden data member, conventionally called __vtbl, which “points to an array of function pointers.” That array is the vtable; the hidden member is the vptr. Every instance of the class shares a single vtable per dynamic type, but each instance carries its own vptr.
A class with no virtual functions has no vptr at all. The vptr is not part of the C++ ABI for non-polymorphic types; it appears the moment the class — or any of its bases — declares a virtual function:
struct PlainOldData {
int x, y;
};
struct Polymorphic {
int x, y;
virtual ~Polymorphic() = default;
};
static_assert(sizeof(PlainOldData) == 8); // two ints
static_assert(sizeof(Polymorphic) == 16); // ints + vptr + padding
On a 64-bit system, the vptr is typically 8 bytes. The Polymorphic struct is twice the size of PlainOldData for what looks like the same payload — the only difference being that Polymorphic declared a virtual destructor.
Where the vptr sits
The vptr’s offset within an object is implementation-defined, but in practice every major compiler that follows the Itanium ABI (GCC and Clang on Linux, macOS, and most Unix targets) places the vptr at offset zero. The data members follow:
Polymorphic object layout (Itanium ABI):
offset 0: [ vptr → vtable for Polymorphic ]
offset 8: [ x (int) ][ y (int) ]
offset 16: [ padding to alignment ]
Putting the vptr first has a practical reason: a virtual call through any base-class pointer can find the vtable at the same offset, with no further arithmetic. MSVC follows the same convention. The placement is so consistent across modern toolchains that it might as well be a rule, even though the standard doesn’t mandate it.
When the vptr is written
The vptr is not set at allocation time. It is written by the constructor — and rewritten at every level of an inheritance hierarchy. Consider:
struct Base {
Base() { initialise(); }
virtual void initialise(){ /* base version */ }
};
struct Derived : Base {
void initialise() override{ /* derived version */ }
};
Derived d; // Which initialise() runs?
Most C++ programmers learn the answer the hard way: Base‘s version. The reason is the vptr write order. When Derived‘s constructor begins, the compiler:
- Constructs the
Basesubobject first, which sets the vptr toBase‘s vtable. - Inside
Base::Base(), the vptr already points atBase‘s vtable, so any virtual call dispatches toBase::initialise. - After
Base::Base()returns, control reachesDerived‘s own constructor body, which rewrites the vptr to point atDerived‘s vtable. - From this point on, virtual calls dispatch normally to
Derived‘s overrides.
The destructor runs the sequence in reverse: Derived‘s vptr is restored to Base‘s vtable as soon as ~Derived finishes and ~Base begins. Virtual calls from a base destructor land in the base’s version, not the derived override.
The practical rule: never call virtual functions from constructors or destructors. The dispatch happens, but it dispatches to the version belonging to whichever class is currently being constructed or destructed — not the one you’d get from outside.
What the vptr costs
The vptr’s presence has three measurable consequences:
- Size. One pointer per object, plus any alignment padding it forces. On 64-bit platforms this is 8 bytes; for objects that contain a single small data member, the vptr can double the object’s size.
- Trivial-type forfeiture. A class with a vptr is not trivially copyable in the standard’s sense.
std::memcpying between two objects is undefined behaviour, even though the bytes look interchangeable, because copying the vptr could theoretically point one object’s vptr at the wrong vtable (the compiler is allowed to assume each object’s vptr matches its dynamic type). Use copy assignment or copy construction. - C-incompatibility. Polymorphic classes can’t be passed by value to C functions or stored in C structs. The vptr is a detail of the C++ object model that has no equivalent in C.
This is why guides that emphasise “you shouldn’t pay for what you don’t use” specifically call out not making things virtual unless you need polymorphism. The cost is real, even if it’s small per object.
Inheritance and shared vptrs
When a derived class shares its layout with a primary base — the first base class that has virtual functions — the two share a single vptr at offset zero. The derived class’s vtable extends the base’s: same entries in the same order, with overrides swapped in, and any new virtual functions appended at the end. This is why a Derived* and the Base* view of the same object are typically the same address.
Multiple inheritance breaks this simplification: each non-primary polymorphic base subobject gets its own vptr at its own offset, and converting between the views shifts the pointer. The thunk machinery from Nibble #066 exists to bridge that gap. The single-vptr-per-polymorphic-base rule is the reason multiple inheritance has costs single inheritance does not.
Inspecting a vptr (when you really want to)
The vptr is hidden by the language but visible to a debugger and to anyone willing to reinterpret the object’s bytes. GCC and Clang’s -fdump-lang-class flag and Clang’s -Xclang -fdump-record-layouts will show you the layout including the vptr position. In a debugger, the vptr usually appears as a synthetic member named _vptr or _vptr$ClassName. It is not intended to be touched from C++ code, but observing it is a quick way to verify your mental model of what the compiler is doing.
Takeaway
A vptr is one hidden pointer per polymorphic object, written by the constructor at each level of an inheritance hierarchy, typically placed at offset zero, and pointing at a single vtable shared across all instances of the same dynamic type. Understanding that the vptr is the object’s record of its own dynamic type explains a great deal: why virtual calls from constructors don’t dispatch the way naive intuition suggests, why polymorphic classes cost a pointer per object, why multiple-inheritance pointer arithmetic is non-trivial, and why std::memcpy is unsafe for polymorphic types. The rule is simple: every virtual you add is a vptr — invisible in source, real in memory