Skip to content

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

$ g++ -std=c++17 -O2 main.cpp -o demo && ./demo
returned  = 1
in memory = -1

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

Open in Compiler Explorer ↗ Quiz this entry