How std::launder Works with Placement New in C++
This article explains how std::launder preserves
compiler optimization capabilities while safely accessing new objects
created via placement new in existing storage. In modern
C++, the compiler relies heavily on strict assumptions regarding the
immutability of const members, references, and dynamic
types to eliminate redundant memory reads and devirtualize calls. When
placement new replaces an object within allocated storage,
reusing existing pointers can invoke undefined behavior. Using
std::launder provides an explicit barrier that resets
compiler provenance analysis for a specific memory address, ensuring
both program correctness and optimization legality.
The Optimization Problem with In-Place Reconstruction
C++ compilers actively perform constant propagation, dead code
elimination, and devirtualization. When a compiler encounters an object
containing const members or reference members, it assumes
that the values of those members will never change throughout the
lifetime of the storage.
Consider a class containing a const member:
struct Resource {
const int id;
Resource(int val) : id(val) {}
};If an instance of Resource is created in memory, the
compiler can legally cache the value of id into a register.
If placement new is subsequently invoked to reconstruct a
new Resource object within the exact same memory address,
the value of id might change. However, if the program
accesses the new object through the original pointer:
Resource* ptr = new Resource(10);
// Read ptr->id -> compiler can assume this is always 10
// Destruct and reuse storage
ptr->~Resource();
new (ptr) Resource(20);
// Undefined Behavior: Compiler may still substitute 10 instead of reading 20
int val = ptr->id; Because the compiler knows ptr points to an object with
a const member, it considers any modification through that
pointer impossible. As an optimization, it replaces
ptr->id with the literal value 10 or a
previously cached register, ignoring the new value written by placement
new.
Object Lifetimes and Pointer Provenance
The C++ standard defines strict rules about when a pointer to an old
object automatically refers to a newly created object in the same
storage. Under [basic.life], a pointer p
pointing to the original object will automatically point to the new
object only if:
- The storage has not been released or reused by a different type (unless certain structural conditions match).
- The new object is of the same type (ignoring top-level cv-qualifiers).
- The type contains no
constnon-static data members and no reference members. - Neither the original nor the new object is a complete
constobject.
If the class contains a const member, a reference
member, or changes dynamic type (such as within an implementation of
std::variant or std::any), the original
pointer loses its validity for accessing the new object. The memory
contains a valid object, but the original pointer cannot legally reach
it because of its associated compiler provenance.
How std::launder Solves the Problem
Introduced in C++17, std::launder takes a pointer and
returns a pointer to the same memory location, but breaks the compiler’s
provenance tracking. It explicitly tells the optimizer: "Do not
assume this pointer holds the properties of the object previously
observed at this address; treat it as pointing to the object that
currently lives there."
Resource* ptr = new Resource(10);
ptr->~Resource();
Resource* new_ptr = new (ptr) Resource(20);
// Correct usage:
int val1 = new_ptr->id; // Valid: uses the pointer returned by placement new
int val2 = std::launder(ptr)->id; // Valid: explicitly launders the old pointerWhile placement new returns a fresh pointer directly
associated with the newly created object, that return value is not
always retained. Low-level data structures, such as contiguous storage
buffers, custom allocators, or tagged unions, frequently store raw byte
arrays or base pointers. In these scenarios, the original pointer is
reused, making std::launder necessary to re-establish
access legality.
Why Compilers Require std::launder
Without std::launder, compilers would be forced to adopt
one of two undesirable strategies:
- Disable Constant Propagation Globally: The compiler
would have to assume that any pointer write or function call could
potentially execute a placement
newthat redefines anyconstor reference member anywhere in memory. This would severely degrade optimization quality across all C++ code. - Accept Silent Data Inconsistencies: The compiler could optimize aggressively without safeguards, leading to silent bugs where reads from rebuilt objects return stale, cached data.
std::launder acts as a targeted optimization barrier. It
allows the compiler to retain hyper-aggressive optimization rules across
the entire codebase by default, shifting the burden to the developer to
explicitly mark locations where storage reuse violates pointer
provenance. By isolating the reuse boundary, the compiler invalidates
its cached assumptions only for the laundered pointer, keeping
surrounding optimizations fully intact and strictly legal.