N38X7
Type Punning Is (Not?!) Real

Published Proposal,

Previous Revisions:
None
Authors:
Paper Source:
GitHub
Issue Tracking:
GitHub
Project:
ISO/IEC 9899 Programming Languages — C, ISO/IEC JTC1/SC22/WG14
Proposal Category:
Change Request, Feature Request
Target:
C2y

Abstract

Type punning should be real or it should not be real. Right now it exists in a sort of pseudo-real, where implementers and experts reply with conflicting opinions and diverge on whether or not union type punning is effectively the same as performing a memory copy and accessing the copied data in the new type.

1. Changelog

1.1. Revision 0 - February 12th, 2026

2. Introduction and Motivation

We have personally claimed that type punning is a legal action in C. It is unambiguously NOT allowed in C++, but vendors fall on different sides of the divide when it comes to the behavior of type punning with a union in C. That is, given the following asserts produce no constraint violations on a given implementation:

#include <stddef.h>
#include <string.h>
#include <stdalign.h>

int main () {
  static_assert(sizeof(float) == sizeof(int), "sizes are not similar");
  static_assert(alignof(float) == alignof(int), "alignments are not similar");
  typedef union u {
    float f;
    int i;
  } u;
#if defined(UNNECESSARY_CHECK) && UNNECESSARY_CHECK == 1
  // Not ncessary: both members start at an offset of `0` due to
  // §6.7.3.2 Structure and union specifiers, ❡18:
  // «A pointer to a union object, suitably converted,
  // points to each of its members (or if a member is a bit-field,
  // then to the unit in which it resides),
  // and vice versa. The members of a union object overlap in such a way
  // that pointers to them when
  // converted to pointers to character type point to the same byte.»
  static_assert(offsetof(u, f) == offsetof(u, i), "offsets are not similar");
#endif
  u pun = {.i = 0};
  const int nopun_i = 0;
  float nopun_f = 12334.0;

  memcpy(&nopun_f, &nopun_i, sizeof(int));
  if (memcmp(&nopun_f, &pun.f, sizeof(int)) == 0) {
    return 0;
  }
  return 1;
}

(Godbolt). Is this program guaranteed to work and return 0? Some implementers say no, and others say yes. This proposal provides 2 pieces of wording attempting to either formalize that wording officially in a "Semantics" section, or to move the type-punning to an explicit "Recommended practice" section so that the divergence is either harmonized in a single, unified direction or explicitly blessed.

NOTE: There was explicit divergence in implementer understanding. Whether or not they have leveraged this into actual implementation divergence or not is not something the author has looked into, but getting conflicting answers from e.g. two Clang developers or two GCC developers has not been very encouraging for the future.

3. Wording

The wording is against the latest working draft.

The intent is to provide 2 options.

3.1. OPTION 1: Modify "§6.5.3.4 Structure and union members" to formalize union semantics

6.5.3.4 Structure and union members
Constraints

The first operand of the . operator shall have an atomic, qualified, or unqualified complete structure or union type, and the second operand shall name a member of that type.

The first operand of the -> operator shall be a pointer to an atomic, qualified, or unqualified complete structure or union type, and the second operand shall name a member of the type pointed to.

Semantics

A postfix expression followed by the . operator and an identifier designates a member of a structure or union object. If the named member is a member of a union and it is not the member last stored into, then the value is the reinterpretation of the appropriate portion with the object representation (6.2.6) of the last stored value represented by the named member. Otherwise, the The value is that of the named member , 94) and . The value is an lvalue if the first expression is an lvalue. If the first expression has qualified type, the result has the so-qualified version of the type of the designated member.

EXAMPLE    The process of reading members not explicitly written to in a union is sometimes called "type punning". This can possibly be a non-value representation. The following fragment returns 0 if no constraints are violated:

#include <stddef.h>
#include <string.h>

int main () {
  static_assert(sizeof(float) == sizeof(int), "sizes are not similar");
  static_assert(alignof(float) == alignof(int), "alignments are not similar");
  typedef union u {
    float f;
    int i;
  } u;

  u pun = {.i = 0};
  const int nopun_i = 0;
  float nopun_f = 12334.0;

  memcpy(&nopun_f, &nopun_i, sizeof(int));
  if (memcmp(&nopun_f, &pun.f, sizeof(int)) == 0) {
    return 0;
  }
  return 1;
}

A postfix expression followed by the -> operator and an identifier designates a member of a structure or union object. The value is that of the named member of the object to which the first expression points, and is an lvalue.95) If the first expression is a pointer to a qualified type, the result has the so-qualified version of the type of the designated member.

Accessing a member of an atomic structure or union object results in undefined behavior.96)

One special guarantee is made to simplify the use of unions: if a union contains several structures that share a common initial sequence (see following sentence), and if the union object currently contains one of these structures, it is permitted to inspect the common initial part of any of them anywhere that a declaration of the completed type of the union is visible. Two structures share a common initial sequence if corresponding members have compatible types (and, for bit-fields, the same widths) for a sequence of one or more initial members.

94)If the member used to read the contents of a union object is not the same as the member last used to store a value in the object the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called type punning). This can possibly be a non-value representation.

3.2. OPTION 2: Modify "§6.5.3.4 Structure and union members" to make union semantics a recommended practice

6.5.3.4 Structure and union members
Constraints

The first operand of the . operator shall have an atomic, qualified, or unqualified complete structure or union type, and the second operand shall name a member of that type.

The first operand of the -> operator shall be a pointer to an atomic, qualified, or unqualified complete structure or union type, and the second operand shall name a member of the type pointed to.

Semantics

A postfix expression followed by the . operator and an identifier designates a member of a structure or union object. The value is that of the named member94) and is an lvalue if the first expression is an lvalue. If the first expression has qualified type, the result has the so-qualified version of the type of the designated member.

A postfix expression followed by the -> operator and an identifier designates a member of a structure or union object. The value is that of the named member of the object to which the first expression points, and is an lvalue.95) If the first expression is a pointer to a qualified type, the result has the so-qualified version of the type of the designated member.

Accessing a member of an atomic structure or union object results in undefined behavior.96)

One special guarantee is made to simplify the use of unions: if a union contains several structures that share a common initial sequence (see following sentence), and if the union object currently contains one of these structures, it is permitted to inspect the common initial part of any of them anywhere that a declaration of the completed type of the union is visible. Two structures share a common initial sequence if corresponding members have compatible types (and, for bit-fields, the same widths) for a sequence of one or more initial members.

Recommended practice

In the case a union member accessed with the . or -> operators, if the named member is not the member last stored into, then the value is the reinterpretation of the appropriate portion with the object representation (6.2.6) of the last stored value represented by the named member. The process of reading members not explicitly or implicitly written to in a union is sometimes called "type punning".

EXAMPLE    Type punning can possibly be a non-value representation. The following fragment returns 0 if no constraints are violated:
#include <stddef.h>
#include <string.h>

int main () {
  static_assert(sizeof(float) == sizeof(int), "sizes are not similar");
  static_assert(alignof(float) == alignof(int), "alignments are not similar");
  typedef union u {
    float f;
    int i;
  } u;

  u pun = {.i = 0};
  const int nopun_i = 0;
  float nopun_f = 12334.0;

  memcpy(&nopun_f, &nopun_i, sizeof(int));
  if (memcmp(&nopun_f, &pun.f, sizeof(int)) == 0) {
    return 0;
  }
  return 1;
}
94)If the member used to read the contents of a union object is not the same as the member last used to store a value in the object the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called type punning). This can possibly be a non-value representation.