Mastering C++ vtable internals: Demystifying polymorphism
This article explains how C++ implements polymorphism through virtual tables and virtual pointers, walks through their generation, memory layout, single‑ and multiple‑inheritance scenarios, distinguishes compile‑time and runtime polymorphism, and shows practical guidelines for when to use or avoid polymorphic designs.
1. Vtable fundamentals
1.1 Generation
During compilation the compiler scans a class definition. If virtual functions are declared it creates a vtable that stores the addresses of those functions in the order of declaration. Pure virtual functions receive a special placeholder entry.
class Base {
public:
virtual void func1() { std::cout << "Base::func1" << std::endl; }
virtual void func2() { std::cout << "Base::func2" << std::endl; }
};1.2 vptr and object layout
Each object that contains virtual functions has a hidden pointer vptr that points to the class's vtable. On a 64‑bit system the vptr occupies 8 bytes and is typically placed at the start of the object. During construction the vptr is initialized to the base‑class vtable; after the derived‑class constructor runs it is updated to the derived vtable. Destruction reverses this process.
1.3 Single‑inheritance example
class Animal {
public:
virtual void speak() { std::cout << "Animal makes a sound" << std::endl; }
virtual ~Animal() {}
};
class Dog : public Animal {
public:
void speak() override { std::cout << "Woof!" << std::endl; }
};Allocate memory for a Dog object, which includes space for the vptr.
The Animal constructor runs; the vptr is set to the Animal vtable.
The Dog constructor runs; the vptr is updated to the Dog vtable, where the speak entry points to Dog::speak.
A base‑class pointer call, e.g. Animal* a = new Dog; a->speak();, uses the vptr to locate the Dog vtable and invokes Dog::speak, achieving runtime polymorphism.
1.4 Multiple‑inheritance (optional)
class Base1 { public: virtual void func1() { std::cout << "Base1::func1" << std::endl; } };
class Base2 { public: virtual void func2() { std::cout << "Base2::func2" << std::endl; } };
class Derived : public Base1, public Base2 {
public:
void func1() override { std::cout << "Derived::func1" << std::endl; }
void func2() override { std::cout << "Derived::func2" << std::endl; }
virtual void func3() { std::cout << "Derived::func3" << std::endl; }
};The Derived object contains two vptrs: the first points to the Base1 vtable, the second to the Base2 vtable. Calls through a Base1* use the first vtable; calls through a Base2* use the second.
2. Polymorphism in C++
2.1 Types of polymorphism
Compile‑time (static) polymorphism : achieved with function overloading and templates.
Runtime (dynamic) polymorphism : realized via virtual functions accessed through base‑class pointers or references.
2.1.1 Overloading example
void print(int num) { std::cout << "Printing int: " << num << std::endl; }
void print(double num) { std::cout << "Printing double: " << num << std::endl; }2.1.2 Template example
template <typename T>
void print(T value) { std::cout << "Printing value: " << value << std::endl; }2.2 Three necessary conditions for runtime polymorphism
Inheritance relationship between a base class and a derived class.
The base class declares a virtual function and the derived class overrides it with the same signature (covariant return types are allowed).
A base‑class pointer or reference actually refers to an object of the derived class.
Violating any condition prevents dynamic dispatch, as demonstrated by the following counter‑examples.
2.2.1 Missing inheritance
class A { public: void func() { std::cout << "A::func" << std::endl; } };
class B { public: void func() { std::cout << "B::func" << std::endl; } };
int main() {
A a; B b;
A* ptr = &a;
ptr->func(); // Calls A::func; no polymorphism.
return 0;
}2.2.2 Non‑virtual function
class Base { public: void func() { std::cout << "Base::func" << std::endl; } };
class Derived : public Base { public: void func() { std::cout << "Derived::func" << std::endl; } };
int main() {
Base* b = new Derived;
b->func(); // Calls Base::func because func is not virtual.
return 0;
}2.2.3 Direct object call
class Animal { public: virtual void speak() { std::cout << "Animal" << std::endl; } };
class Dog : public Animal { public: void speak() override { std::cout << "Woof!" << std::endl; } };
int main() {
Dog dog;
dog.speak(); // Direct call; no dynamic dispatch.
return 0;
}2.3 Typical runtime‑polymorphism scenarios
Graphics rendering : a base class Shape declares a virtual draw. Derived classes Circle, Rectangle, Triangle override it. A helper drawShapes(const Shape* s) calls s->draw(). Adding a new shape only requires a new derived class.
class Shape { public: virtual void draw() const { std::cout << "Drawing a shape" << std::endl; } };
class Circle : public Shape { public: void draw() const override { std::cout << "Drawing a circle" << std::endl; } };
class Rectangle : public Shape { public: void draw() const override { std::cout << "Drawing a rectangle" << std::endl; } };
class Triangle : public Shape { public: void draw() const override { std::cout << "Drawing a triangle" << std::endl; } };
void drawShapes(const Shape* s) { s->draw(); }Game character actions : base Character defines virtual attack. Subclasses Warrior, Mage, Assassin override it. A function performAttack(const Character* c) invokes the appropriate override.
class Character { public: virtual void attack() const { std::cout << "Character attacks" << std::endl; } };
class Warrior : public Character { public: void attack() const override { std::cout << "Warrior slashes with a sword" << std::endl; } };
class Mage : public Character { public: void attack() const override { std::cout << "Mage casts a spell" << std::endl; } };
class Assassin : public Character { public: void attack() const override { std::cout << "Assassin stabs with a dagger" << std::endl; } };
void performAttack(const Character* c) { c->attack(); }Plugin architecture : abstract Plugin with pure virtual execute. Concrete plugins BlurPlugin and FormatConverterPlugin implement it. PluginManager stores std::unique_ptr<Plugin> objects and runs them.
class Plugin { public: virtual void execute() = 0; virtual ~Plugin() {} };
class BlurPlugin : public Plugin { public: void execute() override { std::cout << "Applying blur filter..." << std::endl; } };
class FormatConverterPlugin : public Plugin { public: void execute() override { std::cout << "Converting image format..." << std::endl; } };
class PluginManager {
std::vector<std::unique_ptr<Plugin>> plugins;
public:
void addPlugin(std::unique_ptr<Plugin> p) { plugins.push_back(std::move(p)); }
void runPlugins() { for (const auto& pl : plugins) pl->execute(); }
};Interface‑based sorting : abstract Comparator with pure virtual compare(const void* a, const void* b). Implementations IntegerComparator and StringComparator provide type‑specific logic. A generic sort function receives a Comparator* and uses it to order elements.
class Comparator { public: virtual bool compare(const void* a, const void* b) const = 0; virtual ~Comparator() {} };
class IntegerComparator : public Comparator {
public: bool compare(const void* a, const void* b) const override {
return *static_cast<const int*>(a) < *static_cast<const int*>(b);
}}
;
class StringComparator : public Comparator {
public: bool compare(const void* a, const void* b) const override {
return std::strcmp(*static_cast<const char* const*>(a), *static_cast<const char* const*>(b)) < 0;
}}
;
void sort(void* arr, size_t size, size_t elementSize, const Comparator* comp) {
for (size_t i = 0; i < size - 1; ++i) {
for (size_t j = 0; j < size - i - 1; ++j) {
char* a = static_cast<char*>(arr) + j * elementSize;
char* b = static_cast<char*>(arr) + (j + 1) * elementSize;
if (!comp->compare(a, b)) {
for (size_t k = 0; k < elementSize; ++k) {
char tmp = a[k];
a[k] = b[k];
b[k] = tmp;
}
}
}
}
}3. When to use polymorphism
Polymorphism is valuable for building extensible, maintainable systems where new behaviours need to be added with minimal impact on existing code, such as plugin systems, UI component hierarchies, strategy patterns, or any interface‑based design.
4. When to avoid polymorphism
4.1 Performance‑critical paths
In high‑frequency code (real‑time rendering, tight scientific loops, cryptographic kernels, large matrix multiplication) the indirect virtual call adds measurable overhead. Compile‑time mechanisms (templates, overloads) eliminate this cost.
4.2 Simple, stable hierarchies
If a hierarchy is tiny and unlikely to change (e.g., a fixed set of shapes), using concrete types directly reduces memory for vptrs and removes indirection.
4.3 Deterministic binding required
During initialization or resource‑management phases deterministic, compile‑time binding avoids surprises caused by dynamic dispatch, improving reliability.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Deepin Linux
Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
