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, or std::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 violation

In 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:

  1. Round-Trip Conversions: You may cast a pointer of type A* to B* and then back to A*. Provided B has alignment requirements that are not stricter than A, the original pointer value is preserved, and dereferencing the restored A* pointer is completely safe.
  2. Byte-Level Inspection: The standard explicitly permits aliasing through char*, unsigned char*, and std::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 permitted

Safe 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 constexpr

std::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-defined

Modern 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.