C Mastery / Unions: Representation, Tagged Unions, Type Punning
Part 1 — The Core Language

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

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

Undefined Behavior

implementation-defined (or UB, depending on the reading); it is not portable. STANDARD-VERSION-DEPENDENT

aliasing. VERIFIED

Portability

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

string).

portably.

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.

References

Annex J.

Verification

VERIFIED

verified.`

Progress

Concept checkboxes

Mastery levels

ConceptCurrent level (0–8)Target level
c.union.decl06
c.union.tag06
c.union.punning06