reinterpret_cast vs std::bit_cast: C++ Undefined Behavior

In C++, converting the raw bit representation of an object from one type to another—known as type punning—has historically been prone to bugs and undefined behavior. While developers traditionally turned to reinterpret_cast for this purpose, doing so often violates C++ strict aliasing rules and alignment requirements, resulting in undefined behavior. With C++20, std::bit_cast was introduced to provide a standard-compliant, safe, and constexpr-capable mechanism to reinterpret the binary representation of a value without invoking undefined behavior.

The Problem with reinterpret_cast

reinterpret_cast does not construct a new object or copy bits; instead, it reinterprets the existing pointer or reference type at compile time. When used for type punning (e.g., casting a pointer of one type to a pointer of an unrelated type and dereferencing it), it frequently triggers undefined behavior in two primary ways:

  1. Strict Aliasing Violations: The C++ standard mandates that two pointers of unrelated types cannot point to the same memory location, with narrow exceptions (such as char*, unsigned char*, or std::byte*). If you cast a float* to an int* and dereference it, you violate strict aliasing. Compilers optimize code based on the assumption that unrelated pointers do not alias each other; violating this assumption leads to unexpected optimizations, misordered memory reads, or corrupted program state.
  2. Alignment Faults: Different types have different alignment requirements. If you use reinterpret_cast to convert a pointer to a type that has stricter alignment constraints than the source address guarantees, dereferencing the resulting pointer causes undefined behavior, which can trigger hardware-level exceptions on architectures that strictly enforce alignment.

While reinterpret_cast is valid for converting integer types to pointer types, or converting between unrelated pointer types provided the pointer is cast back to the original type before dereferencing, it is not a safe tool for reading the bits of one type as another.

How std::bit_cast Solves Undefined Behavior

Defined in the <bit> header in C++20, std::bit_cast is explicitly designed for bit-level type reinterpretation. Instead of reinterpreting memory addresses through pointer manipulation, std::bit_cast creates a new object of the destination type and copies the bit representation of the source object into it.

#include <bit>
#include <cstdint>

float f = 1.0f;
// Safe, well-defined bit reinterpretation
auto i = std::bit_cast<std::uint32_t>(f); 

std::bit_cast eliminates undefined behavior through strict compile-time checks:

  • Size Matching: The source and destination types must be of the exact same size (sizeof(To) == sizeof(From)). Mismatched sizes result in a compilation error.
  • Trivially Copyable: Both the source and destination types must be trivially copyable (std::is_trivially_copyable_v<T>). This prevents reinterpreting types that manage dynamic resources, have custom copy constructors, or contain virtual methods.
  • Value-Based Semantics: Because std::bit_cast acts like a standard-compliant std::memcpy on the value level rather than aliasing memory via references or pointers, it does not violate the strict aliasing rule.
  • Compile-Time Execution: Unlike std::memcpy, std::bit_cast is constexpr-qualified, allowing safe bit-level manipulation during compilation without generating runtime overhead or risking undefined behavior.

Summary

The distinction regarding undefined behavior is clear: dereferencing a pointer converted via reinterpret_cast to perform type punning breaks strict aliasing and alignment rules, leading to undefined behavior. In contrast, std::bit_cast provides well-defined value-copy semantics enforced by compile-time constraints, making it the correct choice for reinterpreting bit representations in modern C++.