Unions: Representation, Tagged Unions, Type Punning
This chapter covers union: how it shares storage among members, how to build safe tagged/discriminated unions, and the rules (and dangers) of using unions for type punning.
Why This Matters
Unions are how C represents "this is one of several types, but only one at a time." They are essential for memory-efficient variant types, and their type-punning behavior is a frequent source of undefined-behavior bugs if used incorrectly.
Prerequisites
c.core.26— structs, padding, alignment.
Core Concept
A union shares storage
A union is like a struct, except all members overlap: they share the same storage, and the union's size is the size of its largest member.
union Value {
int i;
float f;
char c;
};
sizeof(union Value) is at least max(sizeof(int), sizeof(float), sizeof(char)). Only one member is "active" at a time (with a special exception for the common initial sequence).
Reading the active member
You should read only the member that was most recently written (the *active* member). Reading a different member is generally implementation-defined or undefined (see below).
Syntax
union Tag {
type member1;
type member2;
};
union Tag u;
u.member1 = value;
Tagged (discriminated) union
A tagged union pairs a union with an enum tag indicating which member is active:
enum Kind { KIND_INT, KIND_FLOAT };
struct Value {
enum Kind kind;
union {
int i;
float f;
} data;
};
Examples
Basic union
#include <stdio.h>
union U {
int i;
float f;
};
int main(void)
{
union U u;
u.i = 42;
printf("%d\n", u.i); /* ok: reading active member */
return 0;
}
Expected output: 42.
Tagged union
#include <stdio.h>
enum Kind { KIND_INT, KIND_FLOAT };
struct Value {
enum Kind kind;
union { int i; float f; } data;
};
void print_value(struct Value v)
{
switch (v.kind) {
case KIND_INT: printf("%d\n", v.data.i); break;
case KIND_FLOAT: printf("%f\n", v.data.f); break;
}
}
int main(void)
{
struct Value a = { KIND_INT, .data.i = 7 };
struct Value b = { KIND_FLOAT, .data.f = 3.5f };
print_value(a);
print_value(b);
return 0;
}
Expected output: 7 then 3.500000.
Type punning via union (implementation-defined/UB in general)
union {
float f;
unsigned int u;
} pun;
pun.f = 1.0f;
unsigned int bits = pun.u; /* NOT portable ISO C in C17 */
In C17, reading a union member other than the last one written is implementation-defined (or UB in some readings); the portable, well-defined way to reinterpret object representation is memcpy into a buffer. C23 clarifies that type punning via union is permitted in some cases. STANDARD-VERSION-DEPENDENT
How It Works
All union members begin at the same address. Writing one member changes the object representation at that address. The union tracks no tag itself — that is why tagged unions require an external enum.
Variations
Anonymous unions (C11)
struct Value {
enum Kind kind;
union { int i; float f; }; /* anonymous: access as v.i, v.f */
};
Common initial sequence
If two structs in a union share an initial sequence of compatible members, you may read those common members through either struct. This is a special exception, not a license to read arbitrary different types. VERIFIED
Common Mistakes
- Forgetting the tag in a tagged union and reading the wrong member.
- Assuming union type punning is always portable ISO C.
- Using unions to break strict aliasing instead of
memcpy. - Confusing union (shared storage) with struct (separate storage).
Undefined Behavior
- Reading a union member other than the active member is, in C17,
implementation-defined (or UB, depending on the reading); it is not portable. STANDARD-VERSION-DEPENDENT
- Accessing a union through an incompatible type in a way that violates strict
aliasing. VERIFIED
Portability
- The size and representation of union members are implementation-defined.
- Union type punning is not portable before C23; prefer
memcpyfor
representation reinterpretation.
Under the Hood
A union object occupies one block of memory at one address, sized for its largest member. The compiler uses the same address for every member; the type of the access determines how many bytes are read and how they are interpreted.
Practical Usage
- Use tagged unions for variant types (e.g., a value that can be int, float, or
string).
- Use
memcpy(not union punning) to reinterpret object representation
portably.
- Use unions to save memory when only one member is needed at a time.
Exercises
1. Define a union with a double and an int, print sizeof, and explain it. 2. Build a tagged union for a numeric value and a print function for it. 3. Reinterpret a float's bits as uint32_t using memcpy and confirm it is portable; contrast with union punning. 4. Demonstrate the common-initial-sequence exception with two structs sharing an initial member.
Deep Challenge
Design a small expression-evaluator value type using a tagged union that can hold an integer, a double, or a heap-allocated string, and write create/ destroy/print functions with correct ownership. Explain the memory and lifetime rules for each variant.
Related Concepts
c.obj.representation— object representation.c.obj.aliasing— strict aliasing.c.struct.decl— structs.c.mem.memcpy— portable representation reinterpretation.
References
- ISO/IEC 9899:2018 §6.7.2.1 (struct/union), §6.5.2.3 (member access),
Annex J.
Verification
- Union members share storage.
VERIFIED - Only one member is active (with common-initial-sequence exception).
VERIFIED
- Union type punning is not portable in C17.
STANDARD-VERSION-DEPENDENT memcpyis the portable way to reinterpret representation.VERIFIED- No example was executed during generation unless noted. `Execution not
verified.`
Progress
- [ ] Read
- [ ] Understand
- [ ] Complete examples
- [ ] Complete exercises
- [ ] Complete deep challenge
Concept checkboxes
- [ ] Union declaration and shared storage
- [ ] Active member rule
- [ ] Tagged/discriminated unions
- [ ] Type punning rules
- [ ] Common initial sequence
Mastery levels
| Concept | Current level (0–8) | Target level |
|---|---|---|
| c.union.decl | 0 | 6 |
| c.union.tag | 0 | 6 |
| c.union.punning | 0 | 6 |