97 Advanced The Hundred
The Write the Compiler Ignored
one slot, two names, two answers
float flip_sign(float* value, unsigned* bits) {
*value = 1.0f;
*bits ^= 0x80000000u; // flip the sign bit: 1.0f becomes -1.0f
return *value;
}
int main() {
float x = 0.0f;
unsigned* raw = reinterpret_cast<unsigned*>(&x); // same 4 bytes, integer view
std::cout << "returned = " << flip_sign(&x, raw) << "\n";
std::cout << "in memory = " << x << "\n";
}
Compile at -O0, then at -O2. What does each build print?
Answer
At -O0 both lines say -1. At -O2 they disagree — the function returns 1, read out
of the very slot that holds -1. Typical output (GCC 11 on x86-64):
Why¶
The strict aliasing rule lets the compiler assume that objects of unrelated types never
share memory, so a store through unsigned* cannot possibly disturb a float. Punning
&x into an unsigned* breaks that promise — undefined behavior — and at -O2,
where GCC switches the assumption on, flip_sign shrinks to three instructions: store the
1.0f, add DWORD PTR [rsi], -2147483648 to flip the sign bit, return the xmm0 it never
bothered to reload. The write landed — in memory = -1 proves it — the read simply declined
to look. Neither -Wall nor -Wextra says a word here, but -Wstrict-aliasing=2 -O2 flags
the cast: "dereferencing type-punned pointer will break strict-aliasing rules". Only
char, unsigned char and std::byte are exempt: they may alias anything.
The fix¶
Copy the bytes instead of renaming them — std::memcpy has no UB, and GCC 11 at -O2
folds both calls into register moves, no library call:
unsigned bits;
std::memcpy(&bits, value, sizeof bits); // read the bits, legally
bits ^= 0x80000000u;
std::memcpy(value, &bits, sizeof bits); // and put them back
return *value; // -1 at every -O level
C++20 spells it std::bit_cast<float>(std::bit_cast<unsigned>(*value) ^ 0x80000000u).
Compiling the original with -fno-strict-aliasing restores -1 too — that is how the
Linux kernel is built — but it dulls the optimizer everywhere, not just here.
Takeaway: casting a pointer doesn't tell the optimizer that the memory is shared — it tells it you promise that it isn't.
Try it: g++ -std=c++17 -O0 main.cpp -o demo && ./demo, then again with -O2