How Virtual Functions Expand C++ Class Size and Shape Memory Layout
This article explains how C++ compilers implement virtual functions using a hidden vptr and virtual table, showing how class size grows from 1 to 8 bytes, detailing memory layouts for single, multiple, and diamond inheritance, and providing commands to dump vtable and layout information.
Class Size without Virtual Functions
An empty class that contains only non‑virtual members occupies at least one byte, guaranteeing a non‑zero stride for arrays of such objects.
class Empty {
public:
Empty() = default;
~Empty() = default;
void hello() { std::cout << "hello world" << std::endl; }
};
// sizeof(Empty) == 1Compilers apply empty‑base optimization so that a base class without data members can occupy zero bytes when used as a base.
Adding a Virtual Function – vptr Insertion
When a virtual function is added, the compiler injects a hidden pointer ( vptr) to a virtual table ( vtbl). On a 64‑bit system the pointer is 8 bytes, so the class size jumps from 1 to 8 bytes.
class Empty {
public:
Empty() = default;
~Empty() = default;
void hello() { std::cout << "hello world" << std::endl; }
virtual void virtual_test() {}
};
// sizeof(Empty) == 8The virtual table stores the addresses of all virtual functions of the class.
Virtual Table Structure and Polymorphic Calls
For a class with virtual functions the compiler creates a vtbl that holds:
offset‑to‑top (type‑conversion offset)
RTTI information
addresses of each virtual function
During construction the constructor writes the address of the class’s vtbl into the vptr. A polymorphic call resolves the function address via the vptr.
class Base {
public:
virtual void virtual_func() {}
};
int main() {
Base *a = new Base();
a->virtual_func(); // polymorphic call (via vptr)
Base b;
b.virtual_func(); // non‑polymorphic call (direct jump)
Base *c = &b;
c->virtual_func(); // polymorphic call through pointer
return 0;
}Assembly for the polymorphic calls fetches the function pointer from the vtbl, while the non‑polymorphic call can jump directly.
Memory Layouts for Different Inheritance Schemes
Single Inheritance
Classes A, B, C form a linear inheritance chain. Each class introduces its own vptr and virtual entries.
class A { int ax; virtual void f0() {} };
class B : public A { int bx; virtual void f1() {} };
class C : public B { int cx; void f0() override {} virtual void f2() {} };
// g++ layout dump (excerpt)
// Vtable for A: 3 entries (offset, RTTI, A::f0)
// Vtable for B: 4 entries (offset, RTTI, A::f0, B::f1)
// Vtable for C: 5 entries (offset, RTTI, C::f0, B::f1, C::f2)
// Class C size = 24, align = 8Multiple Inheritance
When a class inherits from two bases, each base contributes its own vptr. The derived class therefore contains multiple virtual pointers.
class A { int ax; virtual void f0() {} };
class B { int bx; virtual void f1() {} };
class C : public A, public B {
void f0() override {};
void f1() override {};
};
// clang layout dump (excerpt)
// Class C size = 32, align = 8
// Offsets: A part at 0, B part at 16
// vptr_A at 0, vptr_B at 16Diamond (Virtual) Inheritance
With virtual inheritance the shared base appears only once in the most‑derived object, but each intermediate class still holds a vptr for its own sub‑object.
class A { virtual void foo() {} virtual void bar() {} int ma; };
class B : virtual public A { virtual void foo() override {} int mb; };
class C : virtual public A { virtual void bar() override {} int mc; };
class D : public B, public C { virtual void foo() override {} virtual void bar() override {} };
// g++ layout dump (excerpt)
// Class D size = 48, align = 8
// vptr_B at 0, vptr_C at 16, vptr_A at 32
// Members: mb at 8, mc at 24, ma at 40
// The vtable for a class with virtual bases contains extra entries such as vbase_offset and vcall_offset to adjust the this pointer at runtime.Tools to Dump V‑Table and Layout Information
Both clang and g++ provide options to emit class layout and v‑table data.
# clang (LLVM front‑end)
clang++ -cc1 -emit-llvm -fdump-record-layouts -fdump-vtable-layouts main.cpp
# g++ (GCC)
g++ -fdump-class-hierarchy -c main.cpp
# The raw dump is hard to read; pipe through c++filt for demangling:
cat dump.txt | c++filt -n > readable.txtThese commands produce the detailed tables shown above, helping developers understand how virtual functions affect object size and memory representation.
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.
