C++ / a working model

31 / 163   ·   C++20   ·   10 min

Memory model and object lifetime

Keep this sentence

Available storage at an address is not the same as an accessible object already existing there. To decide whether an access is valid, check size, alignment, object lifetime, access type, and bounds together. Construction and destruction determine the object's phase; allocation and deallocation determine the underlying storage's phase.

In this lesson
  1. First distinguish storage, objects, and values
  2. Construction and allocation can be separate
  3. Audit an access along a timeline
  4. Example
  5. Exercise

First distinguish storage, objects, and values

Storage is the region that holds an object. An object has a type, a lifetime, and a representation; a value is determined by the object's state. An object's lifetime usually begins only after you obtain storage of suitable size and alignment and complete initialization. An address that merely looks like a reasonable number does not prove that an accessible object exists there.

An object representation consists of bytes, but bytes are not a free pass between arbitrary types. Casting an address to another pointer type does not automatically construct, guarantee alignment, or permit a violation of type-based access rules. Copying the representation of a trivially copyable type has dedicated guarantees; you should not use that to simulate a string's copy constructor with a byte copy.

Construction and allocation can be separate

Containers typically obtain storage that can hold several elements first, then construct elements one by one. C++20 std::allocator<T>::allocate provides storage for an array and starts the array object's lifetime, but does not construct the elements; std::construct_at constructs the target element at the given location. Existing capacity does not mean elements already exist.

The example allocates one location, constructs a non-throwing Item, accesses its member, then uses std::destroy_at to end its lifetime, and finally returns the storage. After destruction, members are no longer read. These low-level steps are expanded on purpose to explain how containers work; production code should prefer containers directly.

Audit an access along a timeline

When reviewing a pointer, first ask where the object was created, then which operation ends its lifetime, and finally whether any borrow crosses that point. A class object's lifetime ends when its destructor starts to be called; extra access rules apply during destruction, so do not misread this as forbidding the destructor from using members.

Storage can be reused, so equal addresses do not mean object identity stayed the same. Transparent replacement and std::launder are low-level mechanisms with strict conditions; they cannot repair already freed memory. This discussion covers objects and storage. The concurrent memory model also involves data races and visibility; a correct lifetime does not automatically guarantee thread safety.

Pitfalls

  • vector::reserve only grows capacity; you cannot then subscript elements that have not been constructed. You need resize or insertion.
  • If you explicitly destroy an ordinary local object and do not rebuild it according to the rules, the same object may be destroyed again when it leaves scope. Do not treat manual destruction as a general early-release method.

Run an example

Minimum C++20 · complete program · Download .cpp

#include <cassert>
#include <memory>

struct Item {
    int value;
    explicit Item(int n) noexcept : value(n) {}
    ~Item() noexcept {}
};

int main() {
    std::allocator<Item> allocator;
    Item* storage = allocator.allocate(1);
    Item* item = std::construct_at(storage, 42);
    assert(item->value == 42);
    std::destroy_at(item);
    allocator.deallocate(storage, 1);
}

Compile locally

g++ -std=c++20 -Wall -Wextra -Wpedantic -pthread memory-memory-model.cpp -o example && ./example

Expected result

Expected: exit 0, no output; every assert holds.

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

If Item's constructor may throw, how do you ensure the storage obtained by allocate does not leak?

Show a reference answer

Put construct_at in a try block; in catch (...) call allocator.deallocate(storage, 1) and then rethrow. Only after success enter the access, destroy_at, and deallocate path. A failed Item does not need destroy_at, because it never completed construction, but the allocated storage must still be returned. If work after success may also throw, add destructor protection for the already constructed object; real code usually leaves this to a container.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog