C/C++ Arena

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

Your task

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.

Challenge 2

Files and network data often store numbers as bytes in little-endian order (lowest byte first). Write uint32_t read_u32_le(const unsigned char *p) and void write_u32_le(unsigned char *p, uint32_t v) with shifts, so they give the same result on every machine. Don't cast the byte pointer to a uint32_t *.

Challenge 3

A float stores an exponent in bits 23 to 30, with 127 added to it. Write int float_exponent(float f) that returns the real exponent for normal (non-zero) floats: 1.0f gives 0, 8.0f gives 3, 0.5f gives -1. Get the bits with memcpy.

Previous: Floating-point rounding Next: Challenge: hunt the undefined behavior