Every object with a virtual function carries a hidden 8-byte pointer you never wrote. The moment you declare a virtual function in a class, the compiler silently adds a vptr to every instance of that class. You never see it in your code, but sizeof does.
The vptr and the vtable
Two separate things are created the moment a class has a virtual function. It helps to keep them distinct.
The vtable is a per-class lookup table of function pointers, one entry per virtual function in that class. It lives once per class, in the binary’s data segment. Every instance of Dog shares the same Dog vtable. Every instance of Cat shares the same Cat vtable. The table itself is not expensive in terms of memory, it exists once regardless of how many objects you create.
The vptr is different. It lives inside every single object of a class that has virtual functions. It is a pointer to that class’s vtable, stored as a hidden field, typically at the start of the object’s memory layout. This is what sizeof sees.
struct NoVirtual { int x; };
struct WithVirtual { int x; virtual void vFunc(); };
std::cout << sizeof(NoVirtual) << "\n"; // 4
std::cout << sizeof(WithVirtual) << "\n"; // 16 (4 for int + 4 padding + 8 for vptr)
On a 64-bit system, the vptr costs 8 bytes per object. Put a thousand WithVirtual objects in a vector and there are a thousand hidden pointers sitting in your data, one per object, always, with no way to opt out short of removing the virtual function entirely.
How Virtual Dispatch Actually Works
A non-virtual call resolves at compile time. The compiler knows exactly which function to call and emits a direct jump to its address. One instruction, the CPU’s branch predictor handles it trivially.
A virtual call goes through two levels of indirection:
- Load the vptr from the object.
- Load the function pointer from the vtable at the correct offset.
- Call through that pointer.
Two loads before the actual call. On a warm cache this is manageable, the vptr and the vtable entry are likely already in L1. But in a tight loop over a heterogeneous collection, where different objects have different concrete types and their vtables live in different memory locations, those two loads become two potential cache misses per iteration. The CPU’s branch predictor also has to learn indirect call targets, the actual destination address changes depending on the object’s runtime type, which makes speculative execution harder than a direct call whose destination is fixed.
This is why the cost of virtual dispatch is not the indirection itself. It is the cache pressure from chasing pointers across memory, and the compiler’s inability to inline through a call it cannot resolve at compile time.
Multiple Inheritance and Multiple vptrs
A class inheriting from a single base with virtual functions gets one vptr. A class inheriting from multiple bases that each have virtual functions gets multiple vptrs, one per base that introduces its own vtable. The object’s size grows accordingly, and the dispatch mechanism has to pick the right vptr for the right base’s call.
struct A { virtual void foo(); };
struct B { virtual void bar(); };
struct C : A, B {}; // C's objects contain two vptrs
Each base’s interface resolves through its own vptr and its own vtable. This is not a corner case, it is the standard layout for multiple virtual inheritance, and sizeof(C) will reflect it.
Virtual Inheritance and the Diamond Problem
Virtual inheritance is a separate, related mechanism that solves a different problem: the diamond inheritance problem, where a class inherits from two classes that both inherit from the same base, and without intervention you get two separate copies of that base’s data inside the most-derived object.
struct Base { int x; };
struct A : virtual Base {};
struct B : virtual Base {};
struct D : A, B {}; // only one Base subobject inside D
Without virtual on the inheritance, D would contain two Base subobjects, one through A and one through B, with two separate copies of x. With virtual Base declared on both A and B, there is exactly one shared Base subobject inside D. The virtual keyword here goes on the deriving class side, struct A : virtual Base, not on the base class itself. The base class has no say in whether it participates in virtual inheritance.
This shared subobject requires its own pointer to locate it at runtime, since its offset within D cannot be known at compile time from A’s or B’s perspective alone. Virtual inheritance therefore adds further pointer overhead beyond what ordinary virtual functions already add.
The Virtual Destructor Rule
If a class has any virtual functions, its destructor must be virtual too. This is not a performance concern, it is a correctness requirement.
struct Base {
virtual void foo();
~Base(); // not virtual, problem incoming
};
struct Derived : Base {
~Derived(); // this never gets called through a Base*
};
Base* p = new Derived();
delete p; // calls Base::~Base() only, Derived::~Derived() is skipped
When deleting through a base class pointer, the runtime has to know which destructor to actually call. Without a virtual destructor, the call resolves statically to Base::~Base(), and Derived::~Derived() never runs. Any resources Derived owns get leaked. Making the destructor virtual ensures the dispatch mechanism picks the right destructor for the actual runtime type of the object.
The rule is simple: if a class has any virtual function, declare its destructor virtual. No exceptions.
override Is Not Optional Bookkeeping
override tells the compiler that a function is intended to override a virtual function in a base class, and produces an error if it does not match anything in the base.
struct Base {
virtual void process(int x);
};
struct Derived : Base {
void process(int x) override; // compiler checks this matches Base
void process(float x) override; // error: no virtual function to override
};
Without override, a signature mismatch silently creates a new, unrelated function in the derived class rather than overriding the base’s function. The base’s version still gets called through a base pointer, and the derived version never runs. This class of bug is invisible to the compiler unless override is present to give it something to check against.
CRTP as the Zero-Cost Alternative
CRTP, Curiously Recurring Template Pattern, delivers the same polymorphic interface at zero runtime cost. The derived class passes itself as a template argument to the base, which lets the base call into the derived class’s methods without a vtable, a vptr, or any runtime indirection.
template <typename Derived>
struct Animal {
void speak() {
static_cast<Derived*>(this)->speak_impl();
}
};
struct Dog : Animal<Dog> {
void speak_impl() { std::cout << "woof\n"; }
};
struct Cat : Animal<Cat> {
void speak_impl() { std::cout << "meow\n"; }
};
The dispatch resolves at compile time. The compiler knows the concrete type at every call site, which means it can inline speak_impl directly, no indirection, no cache miss from pointer chasing, no vptr in the object. The tradeoff is that you lose the ability to store heterogeneous collections behind a common base pointer at runtime, since Animal<Dog> and Animal<Cat> are different types. CRTP is a compile-time mechanism, not a runtime one, and that constraint is exactly what makes it fast.
In HFT hot paths where the concrete type is known at the call site, which it usually is when the code is structured correctly, CRTP gives full polymorphic interface with none of the virtual dispatch overhead. The moment you find yourself in a profiler looking at cache misses on virtual calls in a tight loop, CRTP is usually the first structural change worth considering.
Quick Reference
Coming from other languages
Most languages with runtime polymorphism implement something equivalent to vtables under the hood. The interface dispatch mechanism in virtually every object-oriented language involves some form of indirect lookup through a per-type table. The difference in C++ is that the cost is visible, sizeof reveals the vptr, the standard says nothing about whether a vtable exists or how it is structured (it is ABI-level, not standard-level), and the programmer has the option to avoid it entirely through templates. Languages that always use virtual dispatch give you no such escape hatch.
The 90% mental model
One virtual function in a class adds a hidden 8-byte vptr to every object of that class, forever. The vtable is shared and cheap. The vptr is per-object and unavoidable once virtual exists. Virtual dispatch costs two pointer loads before the call, inlining is impossible, and the real performance cost in hot paths is cache pressure from following those pointers across memory. If the concrete type is known at compile time, CRTP removes all of this with no runtime cost.