With the adoption of proposals like P2414 (addressing "pointer lifetime-end zap") and the evolving draft specifications in N5054, modern C++ provides a much clearer model for pointer provenance. However, this has led to a common dilemma for systems programmers: what is the actual difference between using std::launder(ptr) and the classic round-trip cast reinterpret_cast<T*>(reinterpret_cast<uintptr_t>(ptr))?

While both expressions are often colloquially described as "provenance barriers" or ways to obtain a valid pointer from an ambiguous or invalidated one, their standard specifications, requirements, and capabilities differ substantially.

1. What Does std::launder Actually Do?

Introduced in C++17 and made constexpr in C++20, std::launder solves a very specific optimization problem: it breaks the compiler's assumption of object identity when an object with const members, reference members, or virtual methods is replaced in-place via placement new.

According to [ptr.launder], calling std::launder(p) returns a pointer to the same storage that points to an object Y, provided:

  • The storage points to an object of dynamic type X where X and T are similar types.
  • Every byte of the storage occupied by Y is reachable through p.
struct Immutable { const int value; };

alignas(Immutable) std::byte storage[sizeof(Immutable)];
auto* a = new (storage) Immutable{42};

// Recreate object in the same storage
a->~Immutable();
auto* b = new (storage) Immutable{99};

// Without std::launder, compiler might optimize a->value to 42
int val = std::launder(a)->value; // Guaranteed 99

2. The Integer Round-Trip: Provenance Recovery Under P2414 / N5054

Prior to P2414, pointing to an object whose lifetime had ended resulted in an "invalid pointer value." In formal terms, using that pointer value (even reading it or passing it around) could technically invoke undefined behavior (the "pointer zap").

P2414 eliminates pointer zap and explicitly specifies that casting an integer representing an address back to a pointer type creates an unspecified choice among all valid pointer values that result in defined behavior:

T* ptr2 = reinterpret_cast<T*>(reinterpret_cast<uintptr_t>(ptr1));

This means integer-to-pointer conversion performs provenance synthesis (or provenance recovery). If valid storage exists at that address, the implementation must produce a pointer holding the provenance necessary to access the object currently residing at that memory address.

3. Key Differences Between std::launder and the Integer Round-Trip

A. Constant Evaluation (constexpr)

The most immediate and practical difference is eligibility for constant evaluation:

  • std::launder is constexpr since C++20.
  • reinterpret_cast is strictly forbidden in constexpr functions (even when casting through uintptr_t).

If you are writing compile-time metaprogramming utilities, std::launder is your only option.

B. Strict Type Matching vs. Arbitrary Casting

std::launder(ptr) has strict type prerequisites. If you pass a T* to std::launder, there must already be an object of dynamic type T (or a similar type) at that memory location. If the object dynamic type does not match, invoking std::launder is immediate undefined behavior.

By contrast, reinterpret_cast<U*>(reinterpret_cast<uintptr_t>(ptr)) completely severs the connection to the original type T. If you allocated storage as raw bytes (e.g., char[]) and subsequently created an object of type Widget, the integer round-trip can produce a valid Widget*. std::launder cannot arbitrarily change the type; it only re-establishes access to an existing object of a similar type.

C. Pointers to Non-Existent Objects

P2414 explicitly validates using integer-to-pointer casts to inspect addresses or perform low-level atomic/volatile accesses to storage where no C++ object of type T currently has its lifetime active.

  • Calling std::launder(p) when no object exists at p causes immediate undefined behavior.
  • Converting an address via uintptr_t allows you to create a pointer suitable for device I/O, volatile writes, or atomic access without an object needing to be legally constructed at that location.

D. Subobject Provenance and Pointer Shrinking

std::launder cannot "widen" pointer provenance beyond the bounds of the original subobject. If ptr pointed to a subobject (like an array element or a struct member), std::launder(ptr) cannot be used to legally navigate backwards to the enclosing class or to adjacent array members.

An integer-to-pointer cast, however, strips all subobject bounds. When casting back from uintptr_t, the implementation selects any pointer value that yields defined behavior, which includes the provenance of the outermost containing complete object.

Summary Comparison

Featurestd::launder(ptr)reinterpret_cast<T*>(reinterpret_cast<uintptr_t>(ptr))
constexpr SupportYes (C++20+)No
Object ExistenceRequires dynamic object of similar type to existDoes not strictly require an active object (e.g., volatile/atomic memory mapping)
Type InvarianceMust preserve similar type TCan cast to any compatible pointer type U*
Provenance EffectRefreshes object identity / devirtualization barrierWipes provenance entirely; synthesizes new provenance for the address

Conclusion

Use std::launder when you are managing lifetimes within standard C++ memory abstractions—such as union-based variants, custom allocators, or in-place object mutation—especially in constexpr code. Reach for the uintptr_t round-trip when dealing with low-level systems programming, hardware-mapped addresses, atomic operations on raw buffers, or when you explicitly need to erase and re-synthesize pointer provenance across unrelated types.