Type Punning in C and C++

Hacker News by 4 min read 40x views
Type Punning in C and C++

Share Post

I had a bug that took me a during to track down. The problem was category punning. A pointer mold worked fine at -O0 and silently bankrupt at -O2. The C vs C++ difference current is genuinely treacherous, and most blog posts on the topic get it wrong.

Type punning is interpreting recollection as distinct types between reads and writes. It’s essential for serialisation, network protocols, and low-level hardware access.

The issue is that “works in practice” and “has defined behaviour” are distinct things.

The Spectrum from Safe to UB

In C, the harmless ways to category pun are union and memcpy. Pointer casts are technically undefined behavior under strict aliasing rules, equal although they activity on all compiler you’ll encounter.

Unions

A union lets you compose as one category and peruse 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 additionally plant beautifully for pulling distinct 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 desire a union, memcpy is harmless and the compiler volition optimise it to a enroll move:

float f = 3.14f; int i; memcpy(&i, &f, sizeof(f)); // defined behavior, compiles to a sole instruction 

Pointer Casts — Convenient but UB

This compiles, runs, and gives you the “right” answer on all platform:

float f = 3.14f; int *p = (int *)&f; int i = *p; 

It’s additionally undefined behavior. The strict aliasing regulation says an entity shall lone be accessed through an lvalue of its productive type, a qualified type of it, or a character type. A pointer mold to an unrelated category violates this.

Why C and C++ Differ

In C, types are a way to construe memory. In C++, types are first-class citizens — the compiler is allowed to assume that distinct types never pseudonym all other.

This has tangible 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* — distinct types — so it assumes they don’t alias. The second c->a == 2 inspect gets optimised distant according to the assumption that penning *u64 = 4 can’t alter c->a. This is technically accurate under the standard, equal although the types do overlap in memory.

The deeper clarification 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 penning C and need to category pun, use a union or memcpy.

Pointer casts “work” until they don’t. And “don’t” method the compiler silently optimises distant the code you idea was executing. If you’re writing C++, the identical applies, affirmative the compiler has additional latitude to break things under the as-if rule.

The bug I started with? A pointer mold from float* to uint32_t* in a hot loop. At -O2, the iteration was optimised under strict aliasing assumptions, and the values I was penning never appeared anywhere I expected them. A union fixed it in ten minutes.

Other Article Hacker News
Close Right Ads
Close Left Ads