How std::launder Informs the C++ Optimizer
In C++, reusing storage via placement new can lead to
undefined behavior when the compiler aggressively optimizes based on
assumptions about an existing object's invariants. Introduced in C++17,
std::launder functions as a pointer provenance barrier that
tells the compiler an object at a specific memory location has been
replaced by a newly created object. This article explains how
std::launder disrupts compiler assumptions regarding
constant propagation and object identity, ensuring safe access to reused
memory buffers.
The Optimizer and Invariant Assumptions
Modern C++ optimizing compilers assume that certain properties of an
object will never change across its lifetime. The most prominent example
involves objects with const data members or reference
members.
When a compiler encounters a pointer to an object containing a
const field, it performs constant propagation. It registers
that the value of this field will remain constant for as long as that
object lives. If the compiler tracks an address and caches a
const value in a CPU register, it will avoid re-reading
that memory location in subsequent operations to maximize execution
speed.
The Problem of Storage Reuse
When you reuse storage using placement new, you end up
ending the lifetime of the original object and starting the lifetime of
a new object within the exact same memory bytes:
struct Resource {
const int id;
};
alignas(Resource) std::byte storage[sizeof(Resource)];
Resource* r1 = new (storage) Resource{10};
// Destroy the first object and create a new one in the same storage
r1->~Resource();
Resource* r2 = new (storage) Resource{20};Under C++ object model rules, r1 and r2
point to the same memory address, but they do not point to the same
object. If code subsequently accesses the memory through r1
(or even a pointer derived directly from the storage address without
formal laundering), the compiler may consider this access to be
targeting the original object.
Because Resource::id is const, the
compiler's optimizer assumes the value remains 10. It may
substitute the literal value 10 directly into instructions,
completely ignoring the fact that placement new wrote the
value 20 into that memory location. Accessing the new
object through the original pointer represents undefined behavior under
[basic.life].
How std::launder Works
std::launder solves this problem by severing the
compiler's tracking of pointer provenance. Defined in
<new>, the function signature is:
template <class T>
constexpr T* launder(T* p) noexcept;When passed a pointer p, std::launder
returns a pointer of type T* that officially points to the
object currently occupying that address, discarding any historical
analysis the optimizer held regarding previous objects in that
storage.
At the compiler level, std::launder acts as an
optimization barrier:
- Destroys Provenance Chains: It instructs the optimizer to drop any assumed relationships between the address and the previous object that inhabited it.
- Invalidates Cached Invariants: It forces the
optimizer to discard cached loads, such as constant-propagated
constmembers, reference targets, or virtual method table pointers (vptrs). - Forces Memory Reloads: Subsequent reads through the laundered pointer are forced to reload values directly from memory rather than assuming register contents remain valid.
// Correct access using std::launder
Resource* valid_ptr = std::launder(reinterpret_cast<Resource*>(storage));
int current_id = valid_ptr->id; // Correctly reloads from memory; yields 20When std::launder Is Strictly Necessary
You do not need std::launder for every placement
new operation. It is strictly required when:
- The type contains
constmembers: The optimizer would otherwise propagate the old constant values. - The type contains reference members: References cannot be reseated; storage reuse creates a new reference that the compiler cannot infer without laundering.
- Polymorphic types change dynamic types: If an
object of derived class
Ais replaced by derived classBin the same base-class storage,std::launderprevents devirtualization optimizations from resolving calls toA's virtual method table. - Accessing storage through a byte array: When
manipulating raw storage classes like
std::optionalorstd::variant, accessing the contained object through the storage address requires laundering to legitimize the pointer conversion.
std::launder does not emit machine instructions or incur
runtime overhead. It exists purely at compile time to restrict optimizer
assumptions and restore language-level guarantees when reusing
memory.