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

