Introduction: The Mystery of Pre-C++23 start_lifetime_as

With C++23, developers gained access to std::start_lifetime_as, a utility designed to explicitly begin the lifetime of objects in raw memory buffers without invoking Undefined Behavior (UB). However, prior to C++23, developers working on low-level serialization or network code had to rely on clever tricks to achieve the same result.

In a well-known CppCon talk titled "Taking a Byte Out of C++ - Avoiding Punning by Starting Lifetimes", presenter Robert Leahy shared a pre-C++23 workaround for starting object lifetimes:

template<typename T>
[[nodiscard]]
T* start_lifetime_as(void* mem) noexcept {
    auto bytes = new (mem) unsigned char[sizeof(T)];
    auto ptr = reinterpret_cast<T*>(bytes);
    (void)*ptr;
    return ptr;
}

At first glance, this function appears to violate strict aliasing and read uninitialized/non-existent objects, triggering UB. How does this code avoid UB under C++20 rules, and why is the seemingly useless (void)*ptr; line included? Let’s break down the C++ memory model rules step-by-step.

1. Implicit Object Creation and P0593R6

To understand why this function works, we must look at C++20 proposal P0593R6 ("Implicit object creation for low-level object manipulation"), which was adopted retroactively into earlier C++ standards.

Prior to P0593R6, C++ required an object to be explicitly constructed (via placement new or constructor call) before its lifetime began. P0593R6 changed this by introducing the concept of implicit-lifetime types (such as aggregate types, scalar types, and trivially copyable types).

The first line of the function:

auto bytes = new (mem) unsigned char[sizeof(T)];

is an array placement new of unsigned char. Under P0593R6, operations that allocate byte arrays or copy bytes (like malloc, memcpy, or placement new of unsigned char[]) implicitly create objects of implicit-lifetime types in the specified storage region if doing so would give the program defined behavior.

2. Does reinterpret_cast Start a Lifetime?

No, performing a reinterpret_cast<T*>(bytes) does not start an object's lifetime by itself. It simply converts the pointer value from unsigned char* to T*.

However, because T is assumed to be an implicit-lifetime type, an object of type T was already implicitly created at the memory location during the placement new operation on line 1. The cast simply creates a pointer targeting that implicitly created T object.

3. Why Is (void)*ptr; Necessary?

The line (void)*ptr; dereferences the pointer and immediately discards the result to suppress compiler warnings. But why is it written in the first place?

Under the C++ object model rules introduced by P0593R6, implicit object creation is determined by whether a set of objects can be implicitly created such that the program execution has defined behavior. The dereference *ptr creates an access expression requiring an object of type T to exist at that address.

  • By accessing *ptr, the program asserts an evaluation that requires a valid object of type T to exist at memory address mem.
  • Because array placement new implicitly creates objects to give the program defined behavior, the standard selects the object creation scenario where an object of type T exists.
  • The (void) cast avoids compiler warnings about unused expressions while ensuring the access requirement remains in the compiled AST logic.

4. Where Does std::launder Fit In?

Placement new of the unsigned char array ends the lifetime of whatever object previously resided at address mem. If a compiler tracks pointer origins (pointer provenance), it might assume that ptr still points to the byte array rather than the newly created object T.

Technically, using std::launder is the standard-compliant way to break pointer provenance and obtain a pointer to a newly created object at an existing address:

return std::launder(reinterpret_cast<T*>(bytes));

In the talk, the presenter avoided std::launder initially to keep the code simpler to explain without diving into pointer provenance right away. However, in strict C++20 practice, using std::launder alongside implicit object creation provides cleaner semantics.

Conclusion: Modern C++23 Recommendation

While the pre-C++23 pattern relies on P0593R6 implicit object creation rules and expression evaluation tricks, modern C++ offers a direct, UB-free, and intention-revealing standard function.

If you are using C++23 or newer, avoid custom workarounds entirely and use the official standard library function:

#include <memory>

T* ptr = std::start_lifetime_as<T>(mem);

For legacy codebases constrained to C++20, using placement new of unsigned char[] combined with std::launder remains the safest approach for handling raw byte buffers.