C++ Inheritance Memory Model

Josh Segal
Geek Culture
Published in
5 min readMay 27, 2021

In this article, we’re going to dive deep into how C++ inheritance looks in memory and how polymorphism works under the hood. This is not an article on best practices and motivations for inheritance, but rather how C++ makes such powerful and fast inheritance tools.

Let’s start with single inheritance.

The Derived class inherits all the member variables and functions of Base, and the objects memory looks like this

The Derived object looks similar to the Base object in memory in the beginning, but then has some extra things that it created like int b.

You might also be wondering where are all the functions are being stored in memory. All function instructions are stored in a special place in memory once, and not per object. When things are compiled, those function calls will point to the location of the function instructions.

How to Override a Function

The next important part of inheritance is the ability for the derived class to override functions defined in the base class. To do this, C++ lets us make functions virtual. When a function is virtual it goes on the virtual table (vtable). The vtable stores function pointers for all the virtual functions. There is one vtable per class and all objects of the class have a vtable pointer to it.

The order of the function pointers from the Base class is the same order as the Derived class. However, the Base vtable’s bar1 function pointer points to Base::bar1 and the Derived vtable’s bar1 function pointer points to Derived::bar1. The Derived vtable will also have all the other virtual functions it created on there, after the virtual functions from the Base class.

A call on a virtual function like d_ptr->bar() is kind of equivalent to *((d_ptr->vtable_ptr)[0])(d_ptr). You don’t actually have access to the vtable_ptr, so this equivalence is theoretical.

  1. d_ptr->vtable_ptr gets the vtable_ptr which points to an array of function pointers.
  2. (d_ptr->vtable_ptr)[0] is the first element on the Derived vtable which gets us a function pointer to Derived::bar1.
  3. *((d_ptr->vtable_ptr)[0]) is the dereferenced function pointer.
  4. *((d_ptr->vtable_ptr)[0])(d_ptr) is calling Derived::bar1 and passing in a reference to the object that called it. The compiler always implicitly passes the “this” pointer to member functions.

Virtual functions get looked up on the object’s vtable at runtime. So unlike regular functions that get marked in at compile time, virtual functions will look up the function on the vtable at runtime, and run whatever function the vtable entry is pointing to. This is a powerful tool in C++ which enables polymorphism. Polymorphism is when you decide somethings functionality at runtime. You can have a pointer to an object call bar1, but the version of bar1 that is actually invoked depends on the object. That’s why the functionality is only determined at runtime.

How come you can set b_ptr = &d?

Why can a Base pointer point to a Derived object? Let’s first answer why this should be true, and then we’ll get to how C++ memory alignment allows this.

All Derived objects are a Base, but not all Base objects are a Derived. So if you have a Base pointer, you’re only going to call functions that are declared in the Base class. Since Derived inherits all the functions and variables from Base, the Derived object will have access to all the functions that Base calls on it, so this is a desired feature.

In order for this to work, the order of the virtual functions and variables needs to be the same in Base and Derived Objects. When a base pointer looks at a Derived object, it just sees the part of it that’s Base.

The red box indicates what a base pointer sees from a Derived object. The b_ptr sees the Base view. The Derived object looks the same as Base on top, and then has a few more things that it created below, but those don’t matter to b_ptr since it isn’t looking at the Base view.

Remember b_ptr->bar1() translates to *((b_ptr->vtable_ptr)[0])(d_ptr) where its calling the first function pointer on the vtable. Regardless of if the underlying object is Derived or Base, the first entry on the vtable is some version of bar1. Because the memory layout of variables and function pointers are the same for Base and the beginning of Derived, b_ptr is able to point to a Derived object and call virtual function bar1 with no problems.

Multiple Inheritance

When you have a Derived class inherit from multiple Bases, the memory gets a little more tricky.

We need to layout the memory so that b1_ptr only has access to the Base1 interface and b2_ptr only has access to the Base2 interface. To do this, we split up the vtables.

The first vtable includes Base1’s virtual functions and Derived’s virtual functions. Next thing in the Derived object is all of the Base1’s fields. Next is vtable_ptr2 which points to all the virtual functions from Base2. Then, all of the Base2 fields followed by the Derived fields.

The reason we split up the vtables is so that the b1_ptr can have a proper Base1 view and the b2_ptr can have a proper Base2 view. We can’t put Base2’s virtual functions in the first virtual table because foo2 needs to be the first thing in the virtual table to get a valid Base2 view, but foo1 is already the first thing in the first virtual table. Thus, we need two virtual tables to support both views.

What’s also interesting is that the address that b2_ptr is pointing to is actually a larger address than what b1_ptr is pointing to. In fact, b1_ptr+sizeof(Base1) = b2_ptr. The compiler will handle the pointer adjustment for you, so don’t worry about that. d_ptr = b2_ptr as well, but d_ptr has a view of the whole Derived object.

Performance

Virtual functions are slightly more expensive than a regular function, so if you’re trying to make an ultra low-latency program, this section is important. Calling a virtual function involves dereferencing the vtable pointer which is an extra lookup that regular functions don’t have to do.

The other thing is that compiler optimizations like inlining can’t be run on virtual function. Inlining is when function code is copied and pasted where a function is called which avoids the creation of a stack frame, pushing arguments on the stack, etc. This can’t be done with virtual functions because the function that’s being run is only determined at run time. These are very small performance hits, but worth knowing.

--

--

Josh Segal
Geek Culture

C/C++ | Computer Systems | Low Latency | Distributed Systems | Computer Networks | Operating Systems