Strict aliasing & type punning
The C compiler does not only translate your loads and stores. It reasons about which
loads and stores can refer to the same object. Strict aliasing is the rule that lets it
assume pointers to unrelated types usually do not alias, and type punning is the act of
looking at one object's bytes as if they were another type. The safe mental model: values
have types, objects have lifetimes and representation bytes, and memcpy is the boring
portable bridge between typed values and raw bytes.
The reset: a
void*or a cast does not erase the rules of the object underneath. It only changes the type of the expression you are about to use.
How it really works
Every C object has an object representation: the bytes that store its value. Character
types can inspect those bytes. That is why unsigned char * is the right tool for byte
dumps, serialization primitives, checksums, and copying opaque payloads.
The stricter rule is about accessing an object through an lvalue. The C standard says an
object's stored value may only be accessed through certain compatible types, qualified
versions of those types, suitable aggregate/union types, or a character type. If you write
a float object and later read the same storage through an int *, the compiler is
allowed to treat that as impossible in strictly conforming C. At optimization levels where
strict aliasing is enabled, that assumption can move, combine, or delete memory operations
in ways that make the program look "miscompiled." The bug is earlier: the program gave
the optimizer a false contract.
This matters even when the addresses are numerically equal:
float f = 1.0f;
int *p = (int *)&f;
printf("%x\n", *p); /* not portable C */
The cast produces an int *, but it does not create an int object at that address. It
also does not promise that the bit pattern of a float is a valid int object read
through an int lvalue. On a common IEEE-754 machine you might get the bits you expected
in a debug build. That is not the rule the optimizer must preserve.
memcpy is different. It copies bytes from one actual object into another actual object:
float f = 1.0f;
uint32_t bits;
memcpy(&bits, &f, sizeof bits);
Now bits is a real uint32_t object, and reading bits uses the correct lvalue type.
The bytes came from the float object representation, but the access is no longer an
incompatible typed access to the float object. Compilers understand this pattern and
usually optimize the copy away.
Union punning is the gray zone people reach for in C. It is common in systems code, and C
has union-specific wording, but it still exposes representation details and can produce
trap or implementation-specific results for some types. Use a union when the union is the
data model. Use memcpy when the operation is "copy these bytes into an object of another
type."
Executable artifact: byte copy, not aliasing by cast
The demo lives in examples/pointers-and-memory/strict-aliasing-and-type-punning/demo.c.
// demo.c - shows portable type punning with `memcpy` and byte inspection via
// `unsigned char`, without violating strict aliasing through pointer casts.
// Compiles cleanly and runs with:
//
// gcc -O0 -Wall -Wextra demo.c -o demo && ./demo
#include <stdint.h>
#include <stdio.h>
#include <string.h>
struct Pair {
uint16_t low;
uint16_t high;
};
static uint32_t float_bits(float value) {
uint32_t bits;
memcpy(&bits, &value, sizeof bits);
return bits;
}
static float float_from_bits(uint32_t bits) {
float value;
memcpy(&value, &bits, sizeof value);
return value;
}
static void dump_bytes(const void *object, size_t size) {
const unsigned char *bytes = object;
printf("object bytes =");
for (size_t i = 0; i < size; i++) {
printf(" %02x", bytes[i]);
}
printf("\n");
}
int main(void) {
float value = 1.0f;
uint32_t bits = float_bits(value);
float restored = float_from_bits(bits);
printf("float 1.0 bits = 0x%08x\n", bits);
printf("restored float = %.1f\n", restored);
struct Pair pair = {.low = 0x1122u, .high = 0x3344u};
dump_bytes(&pair, sizeof pair);
uint32_t combined;
memcpy(&combined, &pair, sizeof combined);
printf("pair copied as u32 = 0x%08x\n", combined);
return 0;
}
Compile and run:
gcc -O0 -Wall -Wextra demo.c -o demo
./demo
Real output:
float 1.0 bits = 0x3f800000
restored float = 1.0
object bytes = 22 11 44 33
pair copied as u32 = 0x33441122
The float bits are copied with memcpy, then copied back into a real float object.
The struct byte dump uses unsigned char, which is allowed to inspect object
representation. The final uint32_t is also created by byte copy. On this little-endian
machine, the bytes print least significant byte first; that byte order is an observation,
not a portable serialization format.
Failure modes & trade-offs
- Casting through a different object type.
T *casts can silence diagnostics while keeping the same undefined behavior. - Assuming
void*is magic.void*is untyped transport for an address. The access after the cast must still respect the real object's type and lifetime. - Packing protocols with struct casts. Network packets, disk blocks, and MMIO layouts often need byte parsing, endian conversion, and alignment handling, not a cast to a C struct pointer.
- Depending on debug behavior.
-O0often hides strict-aliasing bugs because the optimizer is not exploiting its assumptions yet. - Turning off strict aliasing globally.
-fno-strict-aliasingcan be a practical migration switch for old code, but it also removes optimization room from correct code. - Forgetting alignment. Even if aliasing were allowed, dereferencing a misaligned pointer can be undefined or slow on real machines.
In practice
- Use
memcpyfor bit reinterpretation. It says exactly what you mean and optimizes well in modern compilers. - Use
unsigned char *for byte inspection. Do not generalize that exemption to arbitrary pointer types. - Keep storage ownership and typed views close. If an API hands out raw bytes, document which function creates the typed object and when that object's lifetime starts.
- Serialize explicitly. For files and networks, write endian-aware encode/decode functions instead of dumping structs.
- Treat strict-aliasing warnings as smoke, not coverage. The compiler cannot prove every bad alias. Code review still has to know the rule.
- Reach for
-fno-strict-aliasingonly as a boundary decision. It may be reasonable for one legacy library; it should not be a substitute for understanding object access.
Connects to: What a pointer really is · void* and type erasure · Struct layout: alignment & padding · Pointer arithmetic & stride · Pointers & Memory
Sources
- ISO WG14 N1570 — C11 draft — object representation, effective type, aliasing, and byte access rules in standard wording. https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf
- cppreference — Object model — compact reference for effective type, strict aliasing, alignment, and object representation. https://en.cppreference.com/w/c/language/object
- cppreference —
memcpy— documents byte copying as the portable primitive for moving object representations. https://en.cppreference.com/w/c/string/byte/memcpy - GCC manual — Optimize Options — explains
-fstrict-aliasingand the optimizer assumption exposed by the flag. https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html - Jens Gustedt — Modern C — practical C treatment of object representation, aliasing, and portable low-level idioms. https://gustedt.gitlabpages.inria.fr/modern-c/