Structs, unions, enums and typedef · about 50 min

Unions and tagged unions

Why this matters

Sometimes a value can be one of several different things: a token is a number or a name or an operator; a JSON value is null, a boolean, a number, a string, an array or an object; a network message has one of several payload formats. A struct holding all the alternatives side by side wastes memory on the ones not in use. A union overlays them in the same bytes, and a tagged union (a struct with an enum saying which alternative is active plus a union holding it) is the standard C way to represent "one of these". Interpreters (CPython's objects, Lua's values), compilers' syntax trees and the X11 and SDL event systems are all built this way. Unions are also a tool for looking at the raw bytes of a value.

A union shares one block of memory

A union is declared like a struct, but all members start at offset 0:

#include <stdio.h>
#include <stdint.h>

union number {
    int32_t i;
    float f;
    unsigned char bytes[4];
};

int main(void)
{
    union number n;
    printf("sizeof(union number) = %zu\n", sizeof n);

    n.i = 1;
    printf("as int:   %d\n", n.i);
    printf("bytes:    %02x %02x %02x %02x\n", n.bytes[0], n.bytes[1], n.bytes[2], n.bytes[3]);

    n.f = 1.0f;
    printf("as float: %f\n", n.f);
    printf("as int:   %d (0x%08x)\n", n.i, (unsigned)n.i);
    return 0;
}
sizeof(union number) = 4
as int:   1
bytes:    01 00 00 00
as float: 1.000000
as int:   1065353216 (0x3f800000)
  struct (members side by side)          union (members overlap)
  +------+------+-------------+          +---------------------------+
  |  i   |  f   |  bytes[4]   |          | i    (4 bytes)            |
  +------+------+-------------+          | f    (same 4 bytes)       |
   0      4      8         12            | bytes[0..3] (same 4)      |
                                          +---------------------------+
                                           0                         4

Consequences:

Type punning

Reading a member other than the last one written is called type punning. C (unlike C++) explicitly allows it through a union: the bytes are reinterpreted as the new type. It is the standard way to inspect the bits of a float:

uint32_t float_bits(float f)
{
    union { float f; uint32_t u; } pun;
    pun.f = f;
    return pun.u;
}

What you must not do is the pointer version, *(uint32_t *)&f. It breaks the strict aliasing rule (an object may only be accessed through its own type, a character type, or a few other exceptions), which is undefined behavior; optimizing compilers really do miscompile it. The union or memcpy(&u, &f, sizeof u) are the safe ways. The result of punning still depends on the machine (byte order, float format), so it is for low-level code, not portable data exchange.

Tagged unions

A bare union does not know which member is valid, so on its own it is rarely useful. Pair it with an enum, the tag (also called the discriminant), in a struct:

#include <stdio.h>
#include <string.h>

typedef enum { TOK_NUMBER, TOK_IDENT, TOK_OP } TokenKind;

typedef struct {
    TokenKind kind;
    union {
        double number;      /* TOK_NUMBER */
        char ident[16];     /* TOK_IDENT  */
        char op;            /* TOK_OP     */
    } as;
} Token;

void print_token(const Token *t)
{
    switch (t->kind) {
    case TOK_NUMBER: printf("NUMBER %g\n", t->as.number); break;
    case TOK_IDENT:  printf("IDENT  %s\n", t->as.ident);  break;
    case TOK_OP:     printf("OP     %c\n", t->as.op);     break;
    }
}

int main(void)
{
    Token toks[3];
    toks[0].kind = TOK_IDENT;
    strcpy(toks[0].as.ident, "width");
    toks[1] = (Token){ .kind = TOK_OP, .as.op = '*' };
    toks[2] = (Token){ .kind = TOK_NUMBER, .as.number = 2.5 };
    for (int i = 0; i < 3; i++)
        print_token(&toks[i]);
    printf("sizeof(Token) = %zu\n", sizeof(Token));
    return 0;
}
IDENT  width
OP     *
NUMBER 2.5
sizeof(Token) = 24

The layout: 4 bytes of tag, 4 bytes of padding (the union contains a double, so it must be 8-aligned), then the 16-byte union. A struct with all three members separately would be 8 + 16 + 1 + padding = 32 bytes, and with more alternatives the savings grow.

  Token (24 bytes)
  +--------+--------+---------------------------------------+
  |  kind  |  pad   |  as: number (8) / ident (16) / op (1) |
  +--------+--------+---------------------------------------+
   0        4        8                                     24

The rules that make tagged unions safe:

  1. Set the tag and the member together, ideally in one place (a constructor function or a compound literal with designated initializers, like .kind = TOK_OP, .as.op = '*').
  2. Switch on the tag before reading, and read only the member it names.
  3. No default: in those switches, so adding a new kind makes -Wswitch point at every switch that needs a new case.

This is the C version of what other languages build in: Rust's enum, Swift's enums with associated values, TypeScript's discriminated unions, Haskell's algebraic data types. Those languages check rule 2 for you; in C it is discipline.

Anonymous unions

C11 lets you leave the union unnamed inside a struct, so its members are accessed directly:

typedef struct {
    TokenKind kind;
    union {
        double number;
        char ident[16];
        char op;
    };                  /* no name */
} Token2;

/* t.number instead of t.as.number */

Both styles are common. The named version (as, u or data are popular names) makes it obvious at every use that you are reading a union member.

Unions holding pointers

When an alternative owns heap memory (a string of any length, an array of child values), the destroy function must switch on the tag to know what to free:

void value_free(Value *v)
{
    switch (v->kind) {
    case V_TEXT: free(v->as.heap_text); break;
    case V_INT:
    case V_REAL: break;             /* nothing owned */
    }
}

Freeing through the wrong member (treating a number's bits as a pointer) is an invalid free and an abort. This is where the tag discipline matters most.

Common mistakes

Recap

Going further: extend the Value exercise with a V_LIST kind holding a pointer to a heap array of Values and a length. Write value_free recursively, and check it with valgrind. That is, in miniature, how a JSON library represents a document.

Exercises

Exercise 1 · quiz

How big is a union?

On x86-64, what are the sizes of these two unions?

union a { char c; int i; double d; };
union b { char s[10]; int i; };
Answer
  • 8 and 12

All members of a union start at offset 0 and overlap, so a union is as large as its largest member, rounded up to a multiple of its strictest alignment. union a: largest member is the 8-byte double, so 8. union b: the largest member is 10 bytes, but the int requires 4-byte alignment, so the size is rounded up to 12 (so arrays of union b keep the int aligned).

Exercise 2 · write code

Look inside a float

Write

uint32_t float_bits(float f);

that returns the 32 bits of the IEEE 754 representation of f as an unsigned integer, by storing f into a union with a float member and a uint32_t member and reading the other member. (Include <stdint.h> for uint32_t.)

Then make main read floats from standard input until the input ends and print each one as

1 -> 0x3f800000

using printf("%g -> 0x%08x\n", f, (unsigned)float_bits(f));.

File: main.c

Starting point

#include <stdio.h>
#include <stdint.h>

uint32_t float_bits(float f)
{
    return (uint32_t)f;      /* converts the value, not the bits */
}

int main(void)
{
    float f;
    while (scanf("%f", &f) == 1)
        printf("%g -> 0x%08x\n", f, (unsigned)float_bits(f));
    return 0;
}

Example

input

1 -2 0.5 3.75

expected output

1 -> 0x3f800000
-2 -> 0xc0000000
0.5 -> 0x3f000000
3.75 -> 0x40700000
Hint 1

A cast like (uint32_t)f converts the number 1.0 to the integer 1. You want the same 32 bits reinterpreted.

Hint 2

Declare union { float f; uint32_t u; } pun;, set pun.f = f;, and return pun.u;.

Reference solution
#include <stdio.h>
#include <stdint.h>

uint32_t float_bits(float f)
{
    union {
        float f;
        uint32_t u;
    } pun;
    pun.f = f;
    return pun.u;
}

int main(void)
{
    float f;
    while (scanf("%f", &f) == 1)
        printf("%g -> 0x%08x\n", f, (unsigned)float_bits(f));
    return 0;
}

Exercise 3 · write code

A tagged value type

Build a small dynamically typed value, like a variable in a scripting language:

typedef enum { V_INT, V_REAL, V_TEXT } Kind;

typedef struct {
    Kind kind;
    union {
        long i;          /* V_INT  */
        double r;        /* V_REAL */
        char text[32];   /* V_TEXT */
    } as;
} Value;

Input is a sequence of lines int N, real X or text WORD (WORD at most 31 characters). For each line, fill in a Value and print it with a function void print_value(const Value *v) that switches on the tag:

int 42
real 2.500
text "hello"

(%ld, %.3f, and the word in double quotes.) After the input ends, print a summary line

sum 44.500 from 2 numbers, 1 text

where the sum adds all int and real values as a double (%.3f), followed by how many numbers (ints and reals) and how many texts there were. Keep the words exactly as shown (numbers and text) whatever the counts are. An unknown kind word prints unknown kind: WORD and exits with status 1.

File: main.c

Starting point

#include <stdio.h>
#include <string.h>

typedef enum { V_INT, V_REAL, V_TEXT } Kind;

typedef struct {
    Kind kind;
    union {
        long i;          /* V_INT  */
        double r;        /* V_REAL */
        char text[32];   /* V_TEXT */
    } as;
} Value;

void print_value(const Value *v)
{
    /* switch on v->kind */
}

int main(void)
{
    char word[16];
    double sum = 0;
    int numbers = 0, texts = 0;
    while (scanf("%15s", word) == 1) {
        Value v;
        /* fill v according to word, print it, update the summary */
    }
    printf("sum %.3f from %d numbers, %d text\n", sum, numbers, texts);
    return 0;
}

Example

input

int 42
real 2.5
text hello

expected output

int 42
real 2.500
text "hello"
sum 44.500 from 2 numbers, 1 text

Example

input

text a
int -7
text b
real 0.25
int 10

expected output

text "a"
int -7
text "b"
real 0.250
int 10
sum 3.250 from 3 numbers, 2 text
Hint 1

Compare the kind word with strcmp: if (strcmp(word, "int") == 0) { v.kind = V_INT; scanf("%ld", &v.as.i); ... }.

Hint 2

Set the tag and the matching union member together, then call print_value(&v). Only read the member that matches the tag.

Hint 3

In print_value: case V_TEXT: printf("text \"%s\"\n", v->as.text); break;

Reference solution
#include <stdio.h>
#include <string.h>

typedef enum { V_INT, V_REAL, V_TEXT } Kind;

typedef struct {
    Kind kind;
    union {
        long i;          /* V_INT  */
        double r;        /* V_REAL */
        char text[32];   /* V_TEXT */
    } as;
} Value;

void print_value(const Value *v)
{
    switch (v->kind) {
    case V_INT:  printf("int %ld\n", v->as.i);         break;
    case V_REAL: printf("real %.3f\n", v->as.r);       break;
    case V_TEXT: printf("text \"%s\"\n", v->as.text);  break;
    }
}

int main(void)
{
    char word[16];
    double sum = 0;
    int numbers = 0, texts = 0;
    while (scanf("%15s", word) == 1) {
        Value v;
        if (strcmp(word, "int") == 0) {
            v.kind = V_INT;
            if (scanf("%ld", &v.as.i) != 1)
                return 1;
            sum += v.as.i;
            numbers++;
        } else if (strcmp(word, "real") == 0) {
            v.kind = V_REAL;
            if (scanf("%lf", &v.as.r) != 1)
                return 1;
            sum += v.as.r;
            numbers++;
        } else if (strcmp(word, "text") == 0) {
            v.kind = V_TEXT;
            if (scanf("%31s", v.as.text) != 1)
                return 1;
            texts++;
        } else {
            printf("unknown kind: %s\n", word);
            return 1;
        }
        print_value(&v);
    }
    printf("sum %.3f from %d numbers, %d text\n", sum, numbers, texts);
    return 0;
}

Exercise 4 · quiz

Why the tag?

In a tagged union like Value above, what goes wrong if code reads v.as.r when v.kind is V_TEXT?

Answer
  • Nothing is checked: the first 8 bytes of the text are reinterpreted as a double, giving a meaningless number

C keeps no record of which member was last written; the tag is a convention your code maintains. Reading the wrong member reinterprets bytes that were written as something else. For ints and floats that gives nonsense numbers; with pointer members it can crash or worse. Always set the tag and the member together, switch on the tag before reading, and keep the switch free of a default so -Wswitch catches new kinds.