Memory Model Demystified: A Comprehensive Look at C++

Martin Ayvazyan
6 min readAug 25, 2023

Overview

C++ and C are languages known for their low-level access to computer memory. Understanding the memory model is vital not only for performance reasons but also for writing reliable, efficient, and maintainable code. Unlike languages that operate within a managed environment like Java or Python, C++ and C give you almost total control over memory allocation and deallocation. This comes with both power and responsibility. You might have heard about stack and heap, object lifetimes, or memory alignment, but how well do you really understand these concepts?

One powerful, but risky, tool at our disposal for peering into memory is the reinterpret_cast operator. While traditionally used with great caution, reinterpret_cast can serve as an educational lens through which to scrutinize the memory model in action. This article aims to guide you through a series of exercises designed to test understanding of the memory model in C++ and C. It will help to learn how to use reinterpret_cast to access memory in different data models, revealing the nitty-gritty details that a high-level view often conceals.

Before moving forward, it’s worth to mention that even for most of the cases there is a pattern on size of available built-in types, it is anyways platform/compiler specific not being defined by C++ standard. The standard provides high level guarantees on capabilities for the types (like minimum supported range, etc.).

About the strategy

To make the statement clear and visually understandable(which is the best way for C++), for memory models validation will be used the following important components:

reinterpret_cast: type-casting operator that performs a bitwise conversion between types, without changing the bit pattern. In simpler terms, reinterpret_cast takes a pointer or an integral value and "reinterprets" the bits at that address as a pointer or integral value of a different type. It's a powerful tool that gives low-level control over data conversion at compile time but should be used cautiously due to its unsafe nature.

pointer arithmetics: operations that can be performed on pointers to navigate through an array or manipulate memory addresses directly. In C++, the main operations that can be performed on pointers in terms of arithmetic are addition (+), subtraction (-), increment (++), and decrement (--). Multiplication (*) and division (/) are not allowed on pointers. Important to note that by performing arithmetic on a pointer, the compiler automatically scales the size of the increment or decrement by the size of the data type that the pointer is pointing to. This scaling is done based on the sizeof the type.

Note that 0x2 above is for testing/visual demonstration only and by itself is invalid address.

So, char pointer will allow explicitly specify shift amount on bytes and will be used for further testing of memory management.

Memory model of stack-defined variables

On stack, the objects are defined based on order of their declaration, i.e. memory model of the stack is sequential in reverse order. To test the statement:

Few things to note related to stack memory model:

  • The memory order sequential, where sequential refers to the orderly and contiguous placement of objects in memory, one after the other. When a new local variable is declared or a function is called, the memory required is allocated on the top of the stack. As you declare more variables, they get placed right next to the previously allocated ones, filling up the stack in a sequential manner.
  • The order of memory model is reversed based on stack-based nature
  • The objects are destroyed automatically as the closest scope definition comes to the end. Simple illustration to test control block’s memory management

Note: theoretically there are cases when it’s also possible to access caller function’s defined variables, which however heavily depends on platform, compiler and many other factors.

Memory model of class variables

In case of classes and structs too the memory model of member objects is sequential and the order is based on definition(unlike stack-model, is not reversed). To test the statement:

So, sizeof(Base) = sizeof(int_4) + sizeof(bool_1) + padding(3) + sizeof(double_8) + sizeof(string_24) = 40 .

Memory model in case of inheritance

The memory model in case of inheritance(i.e. parent-child relation) is similar to standard memory model mentioned above. It has no relation to the type of inheritance(i.e. private, public, protected). To test the statement:

So, the memory order for the class instances:

  • Inherited classes based on the definition order ( Base1, Base2 in our case)
  • Member objects based on the definition order

Memory model in case of dynamic polymorphism

The class having ≥1 virtual function(s)/destructor is considered ‘polymorphic’ class. In case of polymorphic classes the model mostly the same as in the standard case with one more additional note: virtual pointer for each instance pointing to virtual table of the class. To test the statement:

So, the memory order for the ‘polymorphic’ class instances:

  • Implicit virtual pointer (consider as void*)
  • Inherited classes based on the definition order ( Base1, Base2 in our case)
  • Member objects based on the definition order

Memory model in case of virtual inheritance

In case of virtual inheritance the memory model is not guaranteed to be sequential, i.e. the base class being inherited as virtual can have different memory location, which makes it way more complex to apply pointer/address manipulations to access base class’s members. It heavily depends on the platform and compiler — however, the key component there is the additional virtual pointer for the virtual inherited instance. An example to illustrate/test the statement (may work fine on most of the platforms):

So, here the memory model for Child b is:

  • Virtual pointer for virtual inheritance (consider as void* )
  • Child::m_c1
  • Child::m_y
  • padding for Child::m_y-> void*
  • Virtual pointer/aka v_ptr
  • Base::m_x

Initialization order for member components of class instance

The member objects of class instance are initialized based on definition order by following the memory construction principle described above. To test the statement:

The problem in the above example is incorrect order of initialization of class components, which doesn’t match the memory definiton order. As a result only A::m_z will have well defined value and in case of the rest components the behaviour is undefined.

Summary

Understanding the C++ memory model is crucial for performance optimization, debugging, and writing reliable code. However, directly manipulating memory, especially in cases involving inheritance and polymorphism, should be approached with caution due to the risk of undefined behavior and the potential for non-portable code. The article serves as an educational guide to deepen one’s understanding of how C++ manages memory under the hood, although the techniques discussed (like using reinterpret_cast for memory inspection) should be used cautiously in production code.

C++ is a versatile language that offers a rich learning experience and endless opportunities for fun through its depth, allowing to explore everything from low-level memory management to high-level abstractions.

in the next articles I’ll cover static objects initialization/destruction order, lifetime, compile time evaluation — advantages in scope of safety too.

--

--