C++ VTable Explained: Virtual Function Tables & Dynamic Dispatch

C++ VTable Explained: Virtual Function Tables & Dynamic Dispatch

이 글의 핵심

Overview of the C++ vtable: virtual function pointers, vptr layout, why the first virtual call is indirect, and when CRTP avoids virtual cost.

What is a VTable?

A table that stores pointers to virtual functions.

class Base {
public:
    virtual void func() {}
};

// Memory layout:
// [vptr] -> VTable -> [address of func]

How it works

class Animal {
public:
    virtual void speak() {
        std::cout << "Animal" << std::endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Woof!" << std::endl;
    }
};

// VTable:
// Animal: [speak -> Animal::speak]
// Dog:    [speak -> Dog::speak]

Animal* a = new Dog();
a->speak();  // vptr -> Dog vtable -> Dog::speak

Practical examples

Example 1: Memory layout

#include <iostream>

class Base {
public:
    virtual void func1() {}
    virtual void func2() {}
    int data = 10;
};

int main() {
    Base b;
    std::cout << "size: " << sizeof(b) << std::endl;
    // 8 (vptr) + 4 (data) + padding
}

Example 2: Polymorphism

class Shape {
public:
    virtual void draw() = 0;
    virtual double area() = 0;
    virtual ~Shape() = default;
};

class Circle : public Shape {
    double radius;
public:
    Circle(double r) : radius(r) {}
    
    void draw() override {
        std::cout << "Circle" << std::endl;
    }
    
    double area() override {
        return 3.14 * radius * radius;
    }
};

class Rectangle : public Shape {
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    
    void draw() override {
        std::cout << "Rectangle" << std::endl;
    }
    
    double area() override {
        return width * height;
    }
};

int main() {
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(5));
    shapes.push_back(std::make_unique<Rectangle>(4, 6));
    
    for (auto& shape : shapes) {
        shape->draw();
        std::cout << "Area: " << shape->area() << std::endl;
    }
}

Example 3: Inspecting vptr

class Base {
public:
    virtual void func() {}
};

class Derived : public Base {
public:
    void func() override {}
};

int main() {
    Base b;
    Derived d;
    
    // vptr location (often first 8 bytes)
    void** vptr_b = *(void***)&b;
    void** vptr_d = *(void***)&d;
    
    std::cout << "Base vptr: " << vptr_b << std::endl;
    std::cout << "Derived vptr: " << vptr_d << std::endl;
}

Example 4: Performance comparison

class NonVirtual {
public:
    void func() {}
};

class Virtual {
public:
    virtual void func() {}
};

// Call comparison
NonVirtual nv;
nv.func();  // direct call

Virtual v;
v.func();  // vptr -> vtable -> func (indirect)

Common pitfalls

Pitfall 1: Missing virtual destructor

// Bad: non-virtual destructor
class Base {
public:
    ~Base() {}  // non-virtual
};

class Derived : public Base {
    int* data;
public:
    ~Derived() { delete[] data; }
};

Base* b = new Derived();
delete b;  // Derived destructor not called

// Good: virtual destructor
class Base {
public:
    virtual ~Base() {}
};

Pitfall 2: Virtual calls from constructors

class Base {
public:
    Base() {
        init();  // calls Base::init (no polymorphism in ctor)
    }
    
    virtual void init() {
        std::cout << "Base init" << std::endl;
    }
};

class Derived : public Base {
public:
    void init() override {
        std::cout << "Derived init" << std::endl;
    }
};

Pitfall 3: Performance overhead

// Virtual calls are indirect
for (int i = 0; i < 1000000; i++) {
    obj->virtualFunc();  // vtable lookup
}

// Optimization: final
class Derived final : public Base {
    void func() override final {}
};

Pitfall 4: Object size

class NoVirtual {
    int x;
};  // sizeof may be 4

class WithVirtual {
    int x;
    virtual void func() {}
};  // sizeof includes vptr

VTable-oriented optimizations

// 1. final keyword
class Base {
    virtual void func() {}
};

class Derived final : public Base {
    void func() override final {}
};

// 2. Non-virtual interface (NVI)
class Base {
public:
    void func() {  // non-virtual
        funcImpl();
    }
private:
    virtual void funcImpl() {}
};

FAQ

Q1: When do you get a vtable?

A: For classes that have virtual functions.

Q2: Performance impact?

A:

  • Indirect calls
  • Extra memory (vptr)
  • Possible cache misses

Q3: Is a virtual destructor required?

A: When you delete through a base pointer to a derived object—yes.

Q4: How to optimize?

A:

  • final
  • Non-virtual interface idiom
  • Templates (e.g. CRTP)

Q5: How big is a vtable?

A: Roughly number of virtual functions × pointer size.

Q6: Learning resources?

A:

  • Inside the C++ Object Model
  • Effective C++
  • C++ internals references

More on this topic:

  • Virtual functions and vtable mechanics (series)
  • C++ Virtual Functions
  • C++ CRTP: static polymorphism

Practical tips

Tips you can apply immediately.

Debugging

  • Enable and read compiler warnings first
  • Reproduce with a small test case

Performance

  • Do not optimize without profiling
  • Define measurable targets first

Code review

  • Check common review feedback early
  • Follow team conventions

Practical checklist

Before coding

  • Is this the best fit for the problem?
  • Can teammates maintain it?
  • Does it meet performance needs?

While coding

  • Are warnings cleared?
  • Edge cases covered?
  • Error handling appropriate?

At review

  • Is intent clear?
  • Tests sufficient?
  • Documentation adequate?

Use this checklist to reduce mistakes and improve quality.


Keywords

C++, vtable, virtual function, polymorphism, dynamic dispatch


  • C++ vtable linker error