How Placement New Ends an Object’s Lifetime

Below I provided an example demonstrating how placement new creates multiple objects within the same byte storage. Non-overlapping objects can coexist, while constructing an overlapping object ends the lifetime of the previously existing object. Accessing an object after its lifetime has ended causes undefined behavior.

#include <cstddef>
#include <cstdint>
#include <iostream>
#include <new>

struct Packet final
{
    std::uint64_t Data[4];
};

struct A final
{
    std::uint32_t First;
    std::uint32_t Second;
};

struct B final
{
    std::uint32_t First;
    std::uint32_t Second;
};

struct C final
{
    std::uint32_t Value;
};
static_assert(sizeof(Packet) == 32);
static_assert(sizeof(A) == 8);
static_assert(sizeof(B) == 8);
static_assert(sizeof(C) == 4);

std::uint32_t parse()
{
    // The byte array is the owner of the raw storage. Objects created inside
    // it are nested within the byte array, so the byte array remains alive.
    alignas(Packet) std::byte storage[sizeof(Packet)];

    auto* packet = ::new (storage) Packet{{1, 2, 3, 4}};

    // Packet occupies bytes [0, 32).
    //
    // Creating A in bytes [0, 8) reuses part of Packet's storage.
    // A is nested within the byte array, but it is not nested within Packet.
    // Therefore, the lifetime of the entire Packet object ends here.
    auto* a = ::new (storage) A{10, 20};

    // Creating B in bytes [8, 16) does not overlap A.
    // A and B can therefore be alive at the same time.
    auto* b = ::new (storage + sizeof(A)) B{30, 40};

    // C occupies bytes [4, 8), which overlap the second half of A.
    // C is not nested within A, so creating C ends A's lifetime.
    // B does not overlap C and remains alive.
    auto* c = ::new (storage + sizeof(std::uint32_t)) C{50};

    // Undefined behavior: Packet's lifetime ended when A was created.
    // const auto packetValue = packet->Data[0];

    // Undefined behavior: A's lifetime ended when C was created.
    // const auto aValue = a->First;

    // Well-defined: B and C are still alive and do not overlap.
    return b->First + b->Second + c->Value;

    // Creating one C object would not create an array of C objects:
    //
    //     auto* first = ::new (address) C{1};
    //     std::span<const C> values(first, 10); // Invalid object model.
    //
    // Only one C exists. Constructing a span does not start the lifetime of
    // the remaining nine elements.
}

int main()
{
    std::cout << parse() << '\n'; // Prints 120.
}

Objects layout in the storage

Initially:
[---------------- Packet: bytes 0..31 ----------------]

After creating A and B:
[--- A: 0..7 ---][--- B: 8..15 ---][unused: 16..31]
Packet lifetime has ended.

After creating C:
[A part: 0..3][C: 4..7][--- B: 8..15 ---][unused]
               ↑
Creating C ends A's lifetime.

Using reinterpret_cast

#include <cstddef>
#include <cstdint>
#include <iostream>
#include <new>

struct Packet final
{
    std::uint64_t Data[4];
};

struct A final
{
    std::uint32_t First;
    std::uint32_t Second;
};

struct B final
{
    std::uint32_t First;
    std::uint32_t Second;
};

struct C final
{
    std::uint32_t Value;
};

static_assert(sizeof(Packet) == 32);
static_assert(sizeof(A) == 8);
static_assert(sizeof(B) == 8);
static_assert(sizeof(C) == 4);
static_assert(alignof(Packet) >= alignof(A));
static_assert(alignof(Packet) >= alignof(B));
static_assert(alignof(Packet) >= alignof(C));

std::uint32_t parse(Packet* packet)
{
    // Converting Packet* to std::byte* is allowed. The cast itself does not
    // create new objects and does not end the Packet object's lifetime.
    std::byte* storage = reinterpret_cast<std::byte*>(packet);

    // Calculate all addresses while Packet is still alive.
    void* aAddress = storage;
    void* bAddress = storage + sizeof(A);
    void* cAddress = storage + sizeof(std::uint32_t);

    // A occupies bytes [0..7]. It reuses part of Packet's storage, so the
    // lifetime of the complete Packet object ends here.
    auto* a = ::new (aAddress) A{10, 20};

    // B occupies bytes [8..15]. It does not overlap A, so A and B coexist.
    auto* b = ::new (bAddress) B{30, 40};

    // C occupies bytes [4..7], overlapping A::Second.
    // Constructing C therefore ends A's lifetime. B remains alive.
    auto* c = ::new (cAddress) C{50};

    // Accessing packet or a here would cause undefined behavior because
    // their object lifetimes have ended.
    return b->First + b->Second + c->Value;
}

int main()
{
    Packet packet{{1, 2, 3, 4}};

    std::cout << parse(&packet) << '\n'; // Prints 120.

    // The Packet object's lifetime was ended by parse(), so packet must not
    // be accessed here unless a new Packet object is constructed in place.
}

Leave a Reply

Your email address will not be published. Required fields are marked *