Fundamentals 14 min read

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.

Tech Musings
Tech Musings
Tech Musings
How Virtual Functions Expand C++ Class Size and Shape Memory Layout

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) == 1

Compilers 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) == 8

The 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.

Virtual call assembly
Virtual call assembly

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 = 8

Multiple 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 16

Diamond (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.txt

These commands produce the detailed tables shown above, helping developers understand how virtual functions affect object size and memory representation.

CMemory Layoutvirtual functionsInheritanceVTable
Tech Musings
Written by

Tech Musings

Capturing thoughts and reflections while coding.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.