Union Type Punning in C Across Language Standards

Type-punning via union structures—writing data to one union member and reading it back through another—is a widely used C idiom for reinterpreting the binary representation of data without explicit pointer casting. While commonly used for low-level optimizations and hardware abstraction, the standard validity of union type-punning has shifted over time. Initially classified as unspecified behavior in C89/C90, the practice was formally legitimized via clarifications starting in C99 and remains standard-conforming in C11, C17, and C23, distinguishing C sharply from C++, where the identical idiom constitutes undefined behavior.

C89 / C90: Ambiguity and Unspecified Behavior

In the ANSI C (C89) and ISO C (C90) standards, accessing a union member different from the one last written was not explicitly well-defined for data reinterpretation.

Under Section 6.3.2.3 of C89, reading a union member other than the last stored one yielded unspecified behavior. The standard stated that if a member of a union is accessed after a value has been stored into a different member, the behavior depends on the implementation and may produce an indeterminate value or trigger a hardware trap representation. While major compilers like GCC allowed union-based punning in practice without breaking the strict aliasing rule, the code remained non-portable according to the strict letter of the C89 specification.

C99: Clarification via Technical Corrigendum 3

The release of C99 initially left the status of union type-punning somewhat ambiguous in normative text, leading to debate regarding whether reading an inactive union member violated the strict aliasing rules defined in Section 6.5, paragraph 7.

This ambiguity was formally resolved in ISO/IEC 9899:1999/TC3 (Technical Corrigendum 3), which updated footnote 82 (later footnote 95 in consolidated drafts) attached to Section 6.5.2.3:

"If the member used to read the value of an object is different from the member used to store the value, the corresponding bytes of the object representation of the value are reinterpreted as an object representation of the new type in the quantity of the smaller type (if the types are of different sizes), which is called type-punning. This might yield a trap representation."

With TC3, ISO C explicitly defined union type-punning as valid C code, confirming that unions serve as an explicit exception to the strict aliasing rule.

C11 and C17: Explicit Endorsement

Both C11 (ISO/IEC 9899:2011) and C17 (ISO/IEC 9899:2018) retain the explicit sanction of union type-punning under Section 6.5.2.3 (Structure and union members).

The language semantics guarantee:

  1. Reinterpretation of Bytes: Writing to union_var.member_a and subsequently reading union_var.member_b is standard-defined to read the raw object representation of member_a as if it were of the type of member_b.
  2. Aliasing Protection: The compiler must treat union access as potential aliasing, preventing aggressive optimizer passes from reordering writes and reads between distinct union fields.
  3. Trap Representations: While the access itself is strictly valid, undefined behavior can still occur if the resulting bit pattern constitutes a trap representation for the target type on the host architecture (e.g., signaling NaNs or non-valid pointer addresses on specific architectures).

C23: Continued Support and Modern Refinements

The C23 standard (ISO/IEC 9899:2024) preserves the validity of union type-punning without altering the core semantics established in C99 and C11.

C23 further reinforces predictable type-punning by:

  • Modernizing representation requirements, including the formalization of two's complement as the mandatory signed integer representation, which eliminates negative zero and signed integer trap representations that previously made certain punned integer operations implementation-defined.
  • Retaining footnote guarantees alongside core aliasing rules to ensure that accessing alternative active members through unions remains legal C code.

Key Constraints for Standard Validity

Across all post-C89 standards, type-punning via a union remains strictly valid only under these operational conditions:

  • Shared Storage Overlap: If the destination type is larger than the source type, reading the larger field can read uninitialized padding or memory past the active member, producing indeterminate values. Punning to a type of equal or smaller size avoids this issue.
  • Trap Representations: Reinterpreting raw bytes into a type with stricter bit requirements (such as specific pointer formats or specialized floating-point states) can result in implementation traps.
  • Named Union Requirement: The types involved must be accessed through a union instance visible to the compiler at the point of access. Casting a pointer to an arbitrary type directly to another pointer type and dereferencing it remains a violation of the strict aliasing rule, even if wrapped in conceptual union conversions.