LmCast :: Stay tuned in

Type Punning in C and C++

Recorded: Sept. 22, 2026, 12:09 p.m.

Original Summarized

Type Punning in C and C++ | Personal Workflow Blog

Personal Workflow Blog

About
Comments
Tags Index

Type Punning in C and C++

Sep 21, 2026
• Steve SCHNEPP

c
undefined-behavior
c++

About 7 min read

I had a bug that took me a while to track down. The problem
was type punning. A pointer cast worked fine at -O0 and
silently broke at -O2. The C vs C++ distinction here is genuinely
treacherous, and most blog posts on the topic get it wrong.
Type punning is interpreting memory as different types between reads
and writes. It’s essential for serialisation, network protocols, and
low-level hardware access.
The problem is that “works in practice” and
“has defined behaviour” are different things.
The Spectrum from Safe to UB
In C, the safe ways to type pun are union and memcpy. Pointer casts are technically undefined behavior under strict aliasing rules, even though they work on every compiler you’ll encounter.
Unions
A union lets you write as one type and read as another.
This is defined behavior in C:
union {
float f;
uint32_t bits;
} pun;

pun.f = 3.14f;
uint32_t exp = (pun.bits >> 23) & 0xff; // extract IEEE-754 exponent

This also works beautifully for pulling apart structs:
union {
struct color { float r, g, b, a; } c;
float as_array[4];
} u;

u.c = (struct color){ .r = 1, .a = 1 };
float a = u.as_array[3];

memcpy
If you don’t want a union, memcpy is safe and the compiler will optimise it to a register move:
float f = 3.14f;
int i;
memcpy(&i, &f, sizeof(f)); // defined behavior, compiles to a single instruction

Pointer Casts — Convenient but UB
This compiles, runs, and gives you the “right” answer on every platform:
float f = 3.14f;
int *p = (int *)&f;
int i = *p;

It’s also undefined behavior. The strict aliasing rule says an object shall only be accessed through an lvalue of its effective type, a qualified version of it, or a character type. A pointer cast to an unrelated type violates this.
Why C and C++ Differ
In C, types are a way to interpret memory. In C++, types are first-class citizens — the compiler is allowed to assume that different types never alias each other.
This has concrete consequences. Consider:
struct c {
uint32_t a;
uint32_t b;
};

uint32_t bar(uint64_t *u64, struct c *c) {
if (c->a == 2) {
*u64 = 4;
}

if (c->a == 2) {
return c->a;
}

return c->b;
}

int main() {
struct c c = { 2, 3 };
return bar((uint64_t *) &c, &c);
}

With GCC or Clang at -O2, this returns 2. At -O1 or below, it returns 0. The compiler sees that u64 is uint64_t* and c is struct c* — different types — so it assumes they don’t alias. The second c->a == 2 check gets optimised away based on the assumption that writing *u64 = 4 can’t change c->a. This is technically correct under the standard, even though the types do overlap in memory.
The deeper explanation is in Taking a Byte Out of C++ - Avoiding Punning by Starting Lifetimes, which covers why C++ went this direction.
The Practical Rule
If you’re writing C and need to type pun, use a union or memcpy.
Pointer casts “work” until they don’t. And “don’t” means the compiler
silently optimises away the code you thought was executing. If you’re
writing C++, the same applies, plus the compiler has more latitude to
break things under the as-if rule.
The bug I started with? A pointer cast from float* to uint32_t* in a hot loop. At -O2, the loop was optimised under strict aliasing assumptions, and the values I was writing never appeared where I expected them. A union fixed it in ten minutes.

« GitHub Issue Discussions Belong in Code Comments

Related Posts

Some thoughts I encountered during my working day in the J2EE land. Originally a blog about PWKF, an easy-to-use workflow solution. Yet, as I have less time to work on PWKF than before, it morphed into a generic blog.

Type punning involves interpreting the same memory location as different data types during reads and writes, a practice that is essential in areas like serialization, network protocols, and low-level hardware access. The core difficulty lies in the discrepancy between what appears to work in practice and what is considered defined behavior according to the language standards.

In C, safe methods for achieving type punning involve using unions or memcpy. A union allows a programmer to allocate memory for different types and access the same memory location through different type qualifiers, which is defined behavior. For instance, a union can hold different member types, and operations can be performed on members of the union, demonstrating how memory can be manipulated across type boundaries, such as separating the components of a structure and an array. Alternatively, memcpy provides a safe mechanism; when copying data between different types, the compiler optimizes this operation to a simple register move, ensuring defined behavior regardless of the specific types involved.

Conversely, pointer casts are convenient because they compile and execute without immediate errors across different platforms. However, they are officially undefined behavior because they violate the strict aliasing rule, which stipulates that an object should only be accessed through an lvalue of its effective type, a qualified version of it, or a character type. This distinction is exacerbated by compiler optimizations; pointer casts can appear to work until the compiler leverages assumptions about type separation, causing optimizations that silently break the intended execution flow.

The divergence between C and C++ stems from fundamental differences in how they treat types. In C, types primarily serve as a mechanism for interpreting memory. In contrast, C++ treats types as first-class citizens, granting the compiler greater latitude to assume that different types never alias each other. This distinction has demonstrable consequences, particularly regarding side effects and optimization. For example, in scenarios where functions return different types, such as the example provided, the behavior of pointer manipulation depends heavily on the optimizations applied by the compiler, depending on the level of optimization, such as -O2 versus -O1. In optimized settings, the compiler may assume that writing to one memory location cannot affect the perceived state of another, even when the underlying memory regions overlap, leading to results that deviate from expectations.

The practical rule is to prioritize defined behavior. If the goal is to perform type punning in C, unions or memcpy should be utilized. If working in C++, while pointer casts function, the increased permissiveness of the language means that code relying on this behavior is inherently riskier, as the compiler has more freedom to alter the execution based on aliasing assumptions. If a bug arises due to optimization under strict aliasing assumptions, using a union or memcpy resolves the issue by ensuring the required memory operations adhere to defined rules, rather than relying on the potentially undefined outcomes of a cast.