Containers, allocators, serialisation and low-level optimisation all depend on the exact rules for when an object exists and how it's laid out.
In C++, storage and object lifetime are separate. Memory can exist without an object in it, and an object's lifetime begins only when it's constructed and ends when its destructor runs (or its storage is reused). Accessing an object outside its lifetime is undefined behaviour, even if the bytes are still there.
That's how std::vector works: it allocates raw capacity, then constructs elements in place with placement new, new (ptr) T(args), or C++20's std::construct_at(ptr, args...), and destroys them with std::destroy_at(ptr) without freeing the memory. If you reuse storage in tricky ways (const members, a different type), std::launder may be needed to obtain a valid pointer to the new object.
Layout. Every type has a size and an alignment (alignof). Members are laid out in declaration order with padding inserted so each is properly aligned, and the struct's size is padded to a multiple of its alignment. Ordering members from largest to smallest alignment often shrinks a struct, which matters for big arrays. alignas(64) raises alignment, for example to give a variable its own cache line.
Type properties decide what's legal:
- Trivially copyable types (no custom copy, move or destructor) may be copied with
memcpy. - Standard-layout types have C-compatible layout, and
offsetofworks on them. - Implicit-lifetime types (aggregates of scalars, for example) are created implicitly in memory from
mallocormemcpysince C++20; C++23'sstd::start_lifetime_asdoes it explicitly.
To reinterpret bits, use C++20's std::bit_cast<To>(from) (or memcpy). Reading a float through a uint32_t* violates strict aliasing and is UB. C++20's [[no_unique_address]] lets an empty member take no space, which is how allocators and comparators get stored for free.
Example
#include <bit>
#include <cstdint>
#include <iostream>
#include <memory>
#include <string>
struct Bad { char a; double b; char c; }; // 1 + 7 pad + 8 + 1 + 7 pad = 24
struct Good { double b; char a; char c; }; // 8 + 1 + 1 + 6 pad = 16
struct Empty {};
struct WithTag { [[no_unique_address]] Empty tag; int x; }; // C++20: tag uses no space
int main() {
std::cout << sizeof(Bad) << ' ' << sizeof(Good) << ' ' << alignof(double) << '\n'; // 24 16 8
std::cout << sizeof(WithTag) << '\n'; // 4 on GCC and Clang
alignas(std::string) unsigned char storage[sizeof(std::string)]; // storage, no object yet
std::string* s = std::construct_at(reinterpret_cast<std::string*>(storage), "alive");
std::cout << *s << '\n'; // lifetime has begun
std::destroy_at(s); // lifetime ends; storage remains
float f = 1.0f;
auto bits = std::bit_cast<std::uint32_t>(f); // well-defined type punning
std::cout << std::hex << bits << '\n'; // 3f800000
// *reinterpret_cast<std::uint32_t*>(&f) // UB: strict aliasing violation
}Remember
- Storage and lifetime are separate: construct_at begins it, destroy_at ends it
- Padding follows alignment; order members largest-first to shrink structs
- Use
std::bit_castormemcpyfor type punning, never pointer casts
Watch out for
- Using
memcpyon a type that isn't trivially copyable (e.g. one containing a std::string).