What is the strict aliasing rule and how can it bite you?
This bit-twiddling routine passes at -O0 but returns wrong results once optimization is enabled. Explain why and rewrite it to be well-defined.
#include <cstdint>
float fast_inverse_sqrt_bad(float x) {
std::int32_t i = *reinterpret_cast<std::int32_t*>(&x);
i = 0x5f3759df - (i >> 1);
return *reinterpret_cast<float*>(&i);
}
Find and fix the bug.
Strict aliasing says a stored value may be accessed only through an lvalue of its own type, a compatible type, or char. Reading a float through an int* is undefined behaviour, so the optimizer assumes such pointers never overlap and may reorder or drop loads. The safe tools are std::memcpy and std::bit_cast.
- ✗
reinterpret_cast-ing between unrelated pointer types and dereferencing — textbook UB - ✗Assuming
-O0behaviour proves correctness — strict aliasing breaks only when the optimizer runs - ✗Believing
union-based punning is portable C++ — it is well-defined in C but UB in C++
- →Why is
std::bit_caststrictly better than amemcpy-based pun for trivial types? - →How does
char*get a special exemption from the aliasing rule?
What is wrong with this code?
#include <cstring>
#include <cstdint>
#include <bit>
// WRONG: type punning via reinterpret_cast — UB
float fast_inverse_sqrt_bad(float x) {
std::int32_t i = *reinterpret_cast<std::int32_t*>(&x); // aliasing violation
i = 0x5f3759df - (i >> 1);
return *reinterpret_cast<float*>(&i); // violation again
}
// CORRECT (C++11+): memcpy has no aliasing constraints
float bits_to_float_memcpy(std::int32_t i) {
float f;
std::memcpy(&f, &i, sizeof f); // byte copy, well-defined
return f;
}
// CORRECT (C++20): bit_cast — expressive and constexpr-friendly
float bits_to_float_bitcast(std::int32_t i) {
return std::bit_cast<float>(i);
}
fast_inverse_sqrt_bad accesses a stored float through an lvalue of type std::int32_t. These types are unrelated for aliasing purposes, so each dereference is undefined behaviour. At -O0 it "works", but with optimization on the compiler is entitled to assume int32_t* and float* never alias: it may reorder the write to i relative to the read through &x, cache a stale value, or drop the operation entirely. The result is silent, optimization-level-dependent bugs.
The permitted access types are the object's own type, its cv/signedness-compatible variant, and char/unsigned char/std::byte. To reinterpret a representation, use std::memcpy (since C++11) or std::bit_cast (since C++20) — both are conforming, and bit_cast is additionally usable in constexpr.