Vtable in C++ is the runtime mechanism most compilers use to implement virtual functions and polymorphism. In this beginner-friendly guide, you’ll learn what a vtable is, how it connects to the hidden vptr inside objects, how virtual calls are dispatched, and what to watch out for—using clear code, outputs, and “Try it yourself” challenges after each section.
🔹 What is a Vtable in C++?
A Vtable in C++ (virtual table) is a compiler-generated table of function pointers used for classes with virtual functions. Each polymorphic object stores a hidden pointer (often called vptr) to its class’s vtable. When you call a virtual function through a base pointer/reference, the program looks up the correct function in the vtable of the actual object type at runtime.
- vtable: per-class table of virtual function addresses.
- vptr: per-object pointer to the class’s vtable (set by constructors).
- Virtual call: indirect call via vptr → vtable entry of the most-derived type.
Try it yourself
- Explain in one sentence how vptr and vtable enable dynamic dispatch.
- List two advantages of virtual dispatch (extensibility, clean interfaces) and one cost (indirection).
🔹 First Look: Virtual Call Dispatch via Vtable in C++
This example shows how calls via a base reference invoke the derived override. Under the hood, the call is routed through the object’s vptr to its vtable entry for speak()
.
#include <iostream>
using namespace std;
class Animal {
public:
// makes the class polymorphic (adds vtable)
virtual ~Animal() = default;
// virtual entry in the vtable
virtual void speak() const {
cout << "Animal sound\n";
}
};
class Dog : public Animal {
public:
void speak() const override
{ // overrides vtable entry
cout << "Woof!\n";
}
};
class Cat : public Animal {
public:
void speak() const override {
cout << "Meow!\n";
}
};
int main() {
Dog d;
Cat c;
Animal& a1 = d; // base ref to Dog object
Animal& a2 = c; // base ref to Cat object
a1.speak(); // vtable lookup → Dog::speak
a2.speak(); // vtable lookup → Cat::speak
}
Output
Woof!
Meow!
Takeaway: The static type of the reference is Animal
, but the dynamic type (Dog/Cat) determines which function runs via the Vtable in C++.
Try it yourself
- Add a
Cow
overridingspeak()
and test through anAnimal&
reference. - Remove
virtual
fromAnimal::speak
and observe that calls dispatch to the base version (no vtable dispatch).
🔹 Mental Model: Object Layout and Vptr
While not standardized, most ABIs implement a polymorphic object as:
- A hidden first pointer field (vptr) that points to the class’s vtable.
- Then the class’s non-static data members.
- Each derived type sets vptr to its own vtable in constructors.
This explains why classes with virtual functions usually have a larger size (they store the vptr) than identical classes without virtuals.
#include <iostream>
using namespace std;
struct Plain {
int x;
}; // no virtuals (likely no vptr)
struct Poly {
virtual ~Poly() = default;
int x;
}; // has virtual → likely vptr
int main() {
cout << "sizeof(Plain) = " <<
sizeof(Plain) << "\n";
cout << "sizeof(Poly) = " <<
sizeof(Poly) << "\n";
}
Possible Output (platform-dependent)
sizeof(Plain) = 4
sizeof(Poly) = 16
The exact sizes vary, but Poly
typically has room for the vptr plus alignment. This is a practical way to sense the presence of a vptr for a Vtable in C++.
Try it yourself
- Add another virtual function to
Poly
. Doessizeof(Poly)
change? Why or why not? - Compile on 32-bit vs 64-bit and compare sizes (pointer size affects layout).
🔹 Overriding, final, and Pure Virtuals in the Vtable
Overrides replace the base’s vtable entry. final
prevents further overrides. Pure virtuals (=0
) reserve a slot that must be filled by concrete derived classes before instantiation.
#include <iostream>
using namespace std;
struct Shape {
virtual ~Shape() = default;
// pure virtual (vtable slot reserved)
virtual double area() const = 0;
virtual const char* name() const
{ return "Shape"; } // has default impl
};
struct Circle : Shape {
double r{1.0};
explicit Circle(double rr) : r(rr) {}
double area() const override
{ return 3.1415926535 * r * r; }
const char* name() const final override
{ return "Circle"; } // final
};
int main() {
Circle c(2.0);
Shape& s = c;
cout << s.name() <<
" area=" << s.area() << "\n";
// struct Sub : Circle {
// const char* name() const override { return "X"; }
// }; // ERROR: 'name' is final
}
Output
Circle area=12.5664
Pure virtuals must be overridden before instantiation; otherwise the derived remains abstract. The vtable holds the most-derived implementations where applicable.
Try it yourself
- Create
Rectangle
overriding botharea()
andname()
(not final) and test throughShape&
. - Add another virtual like
perimeter()
and experiment with pure vs defaulted implementations.
🔹 Constructors, Destructors, and Virtual Dispatch
During construction/destruction, the object’s dynamic type is effectively the current base under construction/destruction. Virtual calls made inside constructors/destructors do not dispatch to overrides in more-derived classes (they use the current class’s vtable).
#include <iostream>
using namespace std;
struct Base {
// calls Base::greet (not Derived::greet)
Base() { greet(); }
// calls Base::greet during destruction
virtual ~Base() { greet(); }
virtual void greet() const
{ cout << "Base::greet\n"; }
};
struct Derived : Base {
void greet() const override
{ cout << "Derived::greet\n"; }
};
int main() {
Derived d; // ctor prints "Base::greet"
// end of scope triggers dtor → prints "Base::greet"
}
Output
Base::greet
Base::greet
Guideline: Avoid calling virtual functions in constructors/destructors unless you specifically want base-class behavior. This behavior stems from how the Vtable in C++ is set during object lifetime.
Try it yourself
- Move the virtual call to a non-virtual helper called after construction to see expected overriding behavior.
- Add prints in both
Base
andDerived
dtors to observe destruction order (derived → base).
🔹 Multiple Inheritance and the Vtable
With multiple inheritance, an object can contain multiple base subobjects. A virtual call may require an internal this-pointer adjustment before calling the target function (compiler-generated thunks). The vtable encodes these adjustments so the correct subobject is used.
#include <iostream>
using namespace std;
struct A {
virtual ~A() = default;
virtual void fa() const
{ cout << "A::fa\n"; }
};
struct B {
virtual ~B() = default;
virtual void fb() const
{ cout << "B::fb\n"; }
};
struct C : A, B {
void fa() const override
{ cout << "C::fa\n"; }
void fb() const override
{ cout << "C::fb\n"; }
};
int main() {
C obj;
A* pa = &obj; // points to A subobject
B* pb = &obj; // points to B subobject
// vtable → C::fa (may need no adjust)
pa->fa();
// vtable → C::fb (this-adjustment for the B subobject is handled)
pb->fb();
}
Output
C::fa
C::fb
Your compiler manages pointer adjustments transparently. Just know that multiple inheritance can introduce extra indirection and larger vtables.
Try it yourself
- Add a shared base and make a diamond with virtual inheritance. Observe constructor order and ensure a virtual destructor exists in the top base.
- Static_cast between
A*
andB*
viaC*
and print addresses to see they differ (don’t dereference invalid casts).
🔹 RTTI: dynamic_cast and typeid Use the Vtable
Run-Time Type Information (RTTI) usually stores type metadata alongside the vtable. dynamic_cast
uses this information to safely convert within polymorphic hierarchies; typeid
reports the dynamic type when the base is polymorphic.
#include <iostream>
#include <typeinfo>
using namespace std;
struct Base {
virtual ~Base() = default;
};
struct Derived : Base {};
int main() {
Base* p = new Derived();
if (auto q = dynamic_cast<Derived*>(p)) {
cout << "dynamic_cast succeeded\n";
}
cout << "typeid(*p).name(): " <<
typeid(*p).name() << "\n";
delete p;
}
Possible Output (mangled name varies)
dynamic_cast succeeded
typeid(*p).name(): 7Derived
Note: dynamic_cast
to a pointer returns nullptr
on failure. To references, it throws std::bad_cast
. Both rely on RTTI stored with the vtable in many ABIs.
Try it yourself
- Attempt
dynamic_cast
to the wrong type and handlenullptr
gracefully. - Use
typeid
with a non-polymorphic base and compare results (static vs dynamic type names).
🔹 Performance: Cost and Devirtualization
Virtual calls add a tiny indirection (vptr → vtable) and usually block inlining. Compilers can sometimes devirtualize (turn a virtual call into a direct call) when they prove the exact dynamic type at compile time (LTO, final classes, sealed hierarchies).
- Cost: one pointer load + indirect call; often negligible in non-hot paths.
- Devirtualization: enabled by
final
, whole-program optimization, or obvious dynamic types. - Design tradeoff: virtuals increase flexibility; templates/CRTP give static polymorphism (zero overhead) when types are known at compile time.
Try it yourself
- Create a tight loop with a virtual call and compare to a non-virtual or templated call using
chrono
(microbenchmark carefully). - Mark a class
final
and check if your compiler optimizes the virtual call (inspect assembly if comfortable).
🔹 Pitfalls and Best Practices Around Vtables
- Always make base destructors virtual in polymorphic hierarchies, or deleting via base pointer invokes only the base dtor (leaks/UB).
- Avoid virtual calls in constructors/destructors expecting derived behavior—they dispatch to the current base.
- Don’t manually poke at vtables/vptrs; it’s non-portable and undefined behavior.
- Prefer interfaces (pure virtuals) for extensibility; use
final
where appropriate. - Use RAII to ensure resources are released during stack unwinding (exceptions and virtual dtors play nicely).
Try it yourself
- Create a hierarchy without a virtual destructor, delete through a base pointer, and observe missing derived cleanup (then fix by adding
virtual ~Base()
). - Add
final
to a frequently called virtual to allow more compiler optimizations in some contexts.
🔹 Lab: Observe Effects Without Touching the Vtable
There is no portable way to print a vtable, but you can infer its presence and behavior by size differences, dynamic dispatch results, and RTTI behavior. This small lab cements the core ideas safely and portably.
#include <iostream>
#include <typeinfo>
using namespace std;
struct Base {
virtual ~Base() = default;
virtual const char* who() const
{ return "Base"; }
};
struct Derived : Base {
const char* who() const override
{ return "Derived"; }
};
int main() {
Base b;
Derived d;
cout << "sizeof(Base)=" << sizeof(Base)
<< ", sizeof(Derived)=" <<
sizeof(Derived) << "\n";
Base* p = &d;
cout << "p->who(): " <<
p->who() << "\n";
cout << "typeid(*p).name(): " <<
typeid(*p).name() << "\n";
}
Possible Output (platform-dependent)
sizeof(Base)=8, sizeof(Derived)=8
p->who(): Derived
typeid(*p).name(): 7Derived
The sizes and mangled names will vary, but you’ll consistently see dynamic dispatch and RTTI revealing the derived type—evidence of the Vtable in C++ at work.
Try it yourself
- Add another virtual to
Base
and verify that behavior still dispatches toDerived
. - Replace
virtual
with non-virtual inwho()
and observe the dispatch change (static binding).
🔹 Mini Project: Polymorphic Plugin Interface
Let’s use a vtable-powered interface to run different “plugins” via a base pointer. This shows practical, extensible polymorphism backed by the Vtable in C++.
#include <iostream>
#include <memory>
#include <vector>
using namespace std;
class Plugin {
public:
// virtual dtor for safe cleanup
virtual ~Plugin() = default;
// pure virtual via vtable
virtual const char* name() const = 0;
virtual void run() = 0;
};
class Audio : public Plugin {
public:
const char* name() const override
{ return "Audio"; }
void run() override
{ cout << "[Audio] Mixing...\n"; }
};
class Video : public Plugin {
public:
const char* name() const override
{ return "Video"; }
void run() override
{ cout << "[Video] Rendering...\n"; }
};
void execute(Plugin& p) { // base ref → virtual dispatch
cout << "Running " <<
p.name() << "\n";
p.run();
}
int main() {
vector<unique_ptr<Plugin>> plugins;
plugins.push_back(make_unique<Audio>());
plugins.push_back(make_unique<Video>());
for (auto& p : plugins)
execute(*p); // vtable selects derived run()
}
Output
Running Audio
[Audio] Mixing...
Running Video
[Video] Rendering...
Add new plugins without changing execute
. This separation is a primary benefit of vtable-backed polymorphism—open for extension, closed for modification.
Try it yourself
- Add a
Network
plugin and verify dispatch throughexecute
. - Temporarily remove the virtual destructor and delete via
unique_ptr<Plugin>
to see why a virtual dtor is required (then restore it).
🔹 FAQs about Vtable in C++
Q1: Is the Vtable in C++ part of the standard?
No. The standard specifies behavior (virtual dispatch), not implementation. Vtable/vptr is the de facto implementation used by mainstream compilers/ABIs.
Q2: Do all objects have a vptr?
Only polymorphic objects (types with at least one virtual function) typically store a vptr. Non-polymorphic types do not.
Q3: Does each object store a full vtable?
No. Objects store a single vptr pointing to a shared vtable per class, not a copy of the table per object.
Q4: Are virtual calls slow?
They add a small indirection and usually prevent inlining. For most code, the cost is negligible. Use templates/CRTP for zero-overhead static polymorphism where types are known at compile time.
Q5: Why do virtual calls inside constructors/destructors not dispatch to derived?
Because the dynamic type during construction/destruction is effectively the base under construction/destruction, and the vptr points to the current class’s vtable at those stages.
Q6: How do dynamic_cast
and typeid
relate to vtables?
They rely on RTTI metadata that, in common ABIs, is stored with or next to the vtable for polymorphic types.
Q7: Do I need a virtual destructor?
Yes, if you will delete derived objects via a base pointer/reference. Otherwise only the base destructor runs (leaks/UB).
Q8: Can I print or modify the vtable?
Not portably and not safely. Treat it as an implementation detail; rely on language features instead.
Q9: Does multiple inheritance change vtables?
Yes. Compilers add thunks and pointer adjustments so each base’s virtuals dispatch correctly. You rarely need to worry; the compiler handles it.
Q10: When should I use virtuals vs templates?
Use virtuals (vtable) for runtime polymorphism and plugin-like extensibility. Use templates/CRTP for compile-time polymorphism and zero overhead when types are known at build time.
Try it yourself
- Create a virtual-based renderer interface and a templated math kernel; compare flexibility vs performance.
- Toggle a class to
final
and see if your compiler devirtualizes calls in simple cases.
🔹 Wrapping Up
The Vtable in C++ is the backbone of runtime polymorphism: a per-class table of virtual function pointers that your objects reach via a hidden vptr. It enables clean, extensible designs with minimal overhead. Use virtual destructors in polymorphic bases, avoid virtual calls in constructors/destructors, and choose between runtime (virtual) and compile-time (templates) polymorphism based on your needs. Practice the “Try it yourself” tasks to make these concepts second nature.