Step 7 of 8
Type punning and strict aliasing
Sometimes you need the raw bits of a value: hashing a float, writing it to a network message, inspecting how it's stored. The tempting way is to cast the pointer:
float f = 1.0f;
uint32_t bits = *(uint32_t *)&f; /* undefined behavior */
This is undefined behavior because of the strict aliasing rule: an object may only be read or written through an expression of its own type (or a signed/unsigned version of it, or a character type). A float must not be accessed as a uint32_t.
Why the rule exists
The rule lets the compiler assume that pointers to different types never point at the same memory, and optimize based on that:
int f(int *i, float *fp) {
*i = 1;
*fp = 2.0f;
return *i;
}
GCC with -O2 compiles this to "store 1, store 2.0, return 1": it doesn't read *i again, because a store through a float * can't have changed an int. If a caller passes the same address for both, the function returns 1 even though the memory now holds the bits of 2.0. Casting pointers between unrelated types breaks this assumption, and the optimizer produces code that seems to ignore your writes.
The right way: copy the bytes
memcpy copies bytes, and reading any object's bytes is always allowed. Compilers recognize a small fixed-size memcpy and turn it into a single register move, so it costs nothing:
#include <stdint.h>
#include <stdio.h>
#include <string.h>
int main(void) {
float values[] = {1.0f, -0.0f, 0.5f};
for (int i = 0; i < 3; i++) {
uint32_t bits;
memcpy(&bits, &values[i], sizeof bits);
printf("%5.1f -> %08x\n", values[i], (unsigned)bits);
}
return 0;
}
1.0 -> 3f800000
-0.0 -> 80000000
0.5 -> 3f000000
A float is 32 bits: 1 sign bit, 8 exponent bits and 23 fraction bits (the IEEE 754 format that every mainstream processor uses). That's why -0.0 differs from 0.0 only in the top bit, 0x80000000.
The rules in short
- Reading or writing any object through
char *orunsigned char *is allowed. That's howmemcpy, hashing and serialization code look at bytes. - To reinterpret a value as another type,
memcpyit. In C, reading a different member of a union also works (the unions step covers it). In C++ usestd::bit_cast(C++20) ormemcpy; union punning is undefined behavior there. - GCC and Clang's
-fno-strict-aliasingturns the optimization off, and the Linux kernel is built with it. Portable code shouldn't rely on it.
Your turn: write float_bits(f), returning a float's 32 bits, bits_to_float(bits), the reverse, and sign_bit(f), returning 1 if the sign bit is set (so sign_bit(-0.0f) is 1) and 0 otherwise. Use memcpy, not pointer casts.
Previous: Floating-point rounding Next: Challenge: hunt the undefined behavior