reinterpret_cast Doesn't Start an Object's Lifetime, and That's the Whole Problem
Source: isocpp
The name is part of the problem. reinterpret_cast sounds like it should reinterpret bytes in memory as a different type. That reading is intuitive, consistent with how C programmers have always thought about pointer casts, and wrong in a way that compilers are increasingly willing to exploit against you.
Andreas Fertig’s recent post makes the point directly: reinterpret_cast does not create an object, does not start a lifetime, and does not give you well-defined access to bytes through an incompatible type. What it does is purely mechanical: it tells the compiler to treat a pointer as a pointer to a different type at compile time, without emitting any CPU instruction to accomplish that. On a modern optimizing compiler with -O2, a reinterpret_cast in the output binary is literally nothing, a zero-instruction operation. That might seem harmless, but it means the compiler never sees any evidence that an object of the target type was ever constructed at that address, which matters enormously for what optimizations are legal.
The Strict Aliasing Rule Is the Real Constraint
C++ forbids accessing an object through a pointer to an incompatible type. This is the strict aliasing rule, and it exists so the compiler can assume that two pointers of different types do not alias the same memory. When you write:
float f = 3.14f;
uint32_t bits = *reinterpret_cast<uint32_t*>(&f);
you are reading a float object through a uint32_t*. The types are incompatible. The behavior is undefined. cppreference is unambiguous on this: type aliasing through an incompatible pointer after reinterpret_cast is only defined for char, unsigned char, std::byte, and a small set of other special cases.
The reason this matters in practice is not that the program crashes. It usually doesn’t crash; it runs and produces the expected output on your debug build. The problem is that strict aliasing gives the compiler permission to assume the undefined case never happens, which lets it reorder, hoist, or eliminate loads and stores in ways that produce subtly incorrect behavior only under optimization. GCC and Clang both do this. The -fno-strict-aliasing flag disables the optimization and is why so much legacy embedded and networking code still compiles with it.
A related but distinct constraint is object lifetime. Reading from a region of storage as if it contains a T requires that a T object have been constructed there, not just that the bytes happen to represent a valid T. This is where reinterpret_cast falls apart even for cases that seem to dodge strict aliasing.
What memcpy Actually Fixes
The conventional escape hatch for type punning has been memcpy, and it is well-defined:
float f = 3.14f;
uint32_t bits;
std::memcpy(&bits, &f, sizeof(bits));
This works because memcpy operates on raw bytes and writes them into an existing uint32_t object. You are not accessing a float through a uint32_t*; you are copying bytes from one place to another and reading the destination through its own type. The standard treats this as defined.
The cost is zero on any competent compiler. GCC, Clang, and MSVC all recognize the memcpy-based type-pun pattern and compile it to a single register move or no instruction at all under optimization. The assembly output is identical to the reinterpret_cast version; the difference is entirely in what the language standard allows the compiler to assume.
The weakness of memcpy is ergonomics and context. It reads like a performance operation, not a type conversion. It requires a named destination variable. And it is not constexpr, which rules it out for compile-time computation.
std::bit_cast Closes the Gap for Values
C++20 added std::bit_cast, which is the modern, safe, and constexpr-capable replacement for value-level type punning:
#include <bit>
float f = 3.14f;
uint32_t bits = std::bit_cast<uint32_t>(f);
This is well-defined, produces no UB, and compiles to a register rename or equivalent at -O2. More importantly, it is constexpr, so it works in constant expressions. You can inspect the bit pattern of a float literal at compile time, something neither memcpy nor reinterpret_cast can do.
std::bit_cast requires From and To to be the same size and both trivially copyable. Those constraints are what let the implementation guarantee the copy-then-read semantics without touching memory at all. The standard defines it in terms of creating a new To object from the copied bit representation, not in terms of aliased pointer access, which is exactly why it sidesteps the strict aliasing problem.
For systems programming that involves reading hardware registers, serialization, or protocol buffers, std::bit_cast covers most of what developers were incorrectly using reinterpret_cast for.
The Case std::bit_cast Doesn’t Cover
std::bit_cast returns a new value. Sometimes you cannot afford that. Embedded and systems code frequently deals with a buffer in memory, typically received over a network, read from flash, or DMA’d from a peripheral, that you need to interpret as a struct in place without copying. You want a pointer to the struct, not a copy of it.
Before C++23, the canonical correct approach was placement new plus memcpy:
alignas(MyHeader) unsigned char buf[sizeof(MyHeader)];
// ... buffer filled by DMA or read() ...
MyHeader* hdr = new (buf) MyHeader;
std::memcpy(hdr, buf, sizeof(MyHeader));
Placement new starts the lifetime of a MyHeader object at that address. The subsequent memcpy writes the bytes back, since placement new may not preserve them. This is defined but clunky, and the memcpy is a genuine cost on small microcontrollers where you are trying to avoid exactly that.
std::start_lifetime_as, added in C++23 via P0593, is the direct answer to this:
#include <memory>
alignas(MyHeader) unsigned char buf[sizeof(MyHeader)];
// ... buffer filled externally ...
MyHeader* hdr = std::start_lifetime_as<MyHeader>(buf);
// read through hdr directly, no copy needed
The contract is explicit in the cppreference documentation: it begins the lifetime of a T at the given address without touching the bytes there. This is the precise operation reinterpret_cast was always being misused to express, finally made well-defined. Unlike placement new, it does not initialize the object and does not require writing the bytes back. The storage already contains the correct representation; start_lifetime_as simply tells the compiler that a T object now exists there.
The type must be an implicit-lifetime type, which covers aggregates and scalar types. That covers every real-world protocol struct or hardware register map you would use this for.
The Difference in Terms of What the Compiler Knows
It is worth being precise about why these three operations produce different compiler behavior even when they all compile to zero instructions.
With reinterpret_cast, the compiler sees no object construction at the target address. Subsequent reads through the resulting pointer access a float object through a uint32_t*. The strict aliasing rule says those types do not alias. The compiler is allowed to assume the pointer does not point to the float, meaning it can cache the float’s value in a register and never reload it even after you write through the uint32_t*. This produces the classic aliasing bug: the write appears to have no effect.
With std::start_lifetime_as, the compiler has formal evidence that a T object exists at that address. Reads and writes through the resulting pointer are reads and writes to a T, which is what the pointer type says. There is no aliasing violation, no license to assume stale cache values are still current.
With std::bit_cast, you never have a pointer into the original object’s storage at all. The return value is a new object; reading from it is always through its own type. No aliasing is possible by construction.
Where reinterpret_cast Still Belongs
None of this means reinterpret_cast is useless. It has legitimate uses where strict aliasing is not the concern. Casting between pointer and integer types (uintptr_t conversions) is defined. Casting void* back to the concrete type that was originally stored there is defined. Casting between pointer types when working with hardware MMIO registers, where you control the memory layout and the compiler cannot observe the aliasing, is common in embedded codebases that already carry -fno-strict-aliasing.
The problem is the mismatch between what developers think the cast does and what the standard actually guarantees. The cast is defined at the pointer level; the dereference is where the aliasing rules apply. Developers who write reinterpret_cast with the mental model of “view these bytes as a different type” are thinking about the dereference semantics, not the pointer semantics, and the two are not the same thing.
What Fertig’s Point Really Comes Down To
The lesson in the original post is that reinterpret_cast is a pointer-rebinding operation, not a type-punning operation. The distinction matters because pointer rebinding moves UB from the cast site to the dereference site. Those sites can be arbitrarily far apart in complex code, which is why these bugs surface so late and are so hard to reproduce outside of release builds.
For embedded work and systems programming in C++23, the pattern to adopt is: receive raw bytes into unsigned char or std::byte storage, call std::start_lifetime_as to get a well-typed pointer, and read through that pointer. For value-level type punning in C++20 and later, use std::bit_cast. For anything older, use memcpy and accept the slightly more verbose call site.
The tooling to write this code correctly without sacrificing performance now exists. The remaining obstacle is the accumulated muscle memory of developers who learned from C examples that predate the strict aliasing optimizations that make the UB consequential.