reinterpret_cast and Strict Aliasing in C++
In C++, developers frequently assume that
reinterpret_cast grants permission to read an object
through an arbitrary pointer type, but doing so often produces undefined
behavior under the language's strict aliasing rules. While
reinterpret_cast permits you to convert between unrelated
pointer and reference types at compile time, it does not exempt the
resulting access from aliasing constraints. This article explains how
reinterpret_cast interacts with the strict aliasing rule,
why dereferencing such casts can lead to compiler miscompilations, and
how to safely inspect an object's raw binary representation using modern
C++ idioms.
Understanding the Mechanics of reinterpret_cast
The reinterpret_cast operator is a purely compile-time
directive. It instructs the compiler to treat the bit representation of
a pointer or reference as if it were a pointer or reference to a
different type.
Crucially, performing a reinterpret_cast from
TypeA* to TypeB* does not convert the
underlying object, nor does it create a new object of
TypeB. It merely produces a new pointer value pointing to
the original memory location. The cast itself is valid syntax and
well-defined behavior; the problem arises entirely when that resulting
pointer is dereferenced.
What Is the Strict Aliasing Rule?
The strict aliasing rule is a core C++ assumption that allows optimizing compilers to generate efficient machine code. Under this rule, a compiler assumes that pointers of different, unrelated types cannot point to the same memory location (they cannot "alias" each other).
Because the compiler assumes distinct types occupy distinct locations in memory, it is free to cache values in CPU registers, reorder reads and writes, and eliminate seemingly redundant memory operations.
The C++ standard defines a narrow set of conditions under which an object can be legally accessed through a glvalue of a different type:
- The dynamic type matches the accessing type (or is a signed/unsigned variant of it).
- The accessing type is a base class of the dynamic type.
- The accessing type is a character type:
char,unsigned char, orstd::byte.
The Interaction: The Aliasing Violation
A common antipattern involves casting a pointer to re-read the underlying bytes as a different scalar type:
float f = 5.0f;
int* i = reinterpret_cast<int*>(&f); // Well-formed: pointer conversion is allowed
int value = *i; // Undefined Behavior: strict aliasing violationIn this example, the assignment to i succeeds without
error. However, dereferencing *i violates strict aliasing
because int and float are completely unrelated
types.
Because the compiler relies on strict aliasing for optimization, it
assumes that writes to f cannot affect reads from
i. In optimized builds (-O2 or
-O3 in GCC and Clang), the compiler may reorder
instructions, read uninitialized memory, or eliminate code paths
entirely, leading to subtle and unpredictable runtime bugs.
Permissible Uses of reinterpret_cast
Dereferencing a reinterpret_cast pointer is not always
undefined behavior. It is valid under specific, standard-mandated
scenarios:
- Round-Trip Conversions: You may cast a pointer of
type
A*toB*and then back toA*. ProvidedBhas alignment requirements that are not stricter thanA, the original pointer value is preserved, and dereferencing the restoredA*pointer is completely safe. - Byte-Level Inspection: The standard explicitly
permits aliasing through
char*,unsigned char*, andstd::byte*. Casting any object pointer to these types allows you to read its raw byte representation:
float f = 5.0f;
auto byte_ptr = reinterpret_cast<const std::byte*>(&f);
std::byte first_byte = *byte_ptr; // Well-defined: byte-type aliasing is permittedSafe Alternatives for Type Punning
If you need to interpret the bit pattern of one type as another (type
punning), you must avoid dereferencing reinterpret_cast
pointers. Modern C++ provides safe, defined mechanisms for this
purpose.
std::bit_cast (C++20)
In C++20 and later, std::bit_cast (found in the
<bit> header) is the preferred method for bit
reinterpretation. It operates at compile time when possible and avoids
aliasing violations altogether:
#include <bit>
float f = 5.0f;
int i = std::bit_cast<int>(f); // Safe, standard-compliant, and can be constexprstd::bit_cast requires that both types have the exact
same size and are trivially copyable.
std::memcpy
For codebases using C++17 or earlier, copying the bytes via
std::memcpy is the standard-compliant technique:
#include <cstring>
float f = 5.0f;
int i;
static_assert(sizeof(f) == sizeof(i));
std::memcpy(&i, &f, sizeof(f)); // Well-definedModern optimizing compilers recognize std::memcpy
patterns involving small, trivially copyable types and replace the
library call with a direct register transfer, generating assembly that
is identical to an unchecked cast without triggering undefined
behavior.