Structs, unions, enums and typedef · about 45 min

Enums and typedef

Why this matters

Programs are full of values that come from a small fixed set: the state of a connection (connecting, open, closing, closed), the kind of a token (number, identifier, operator), a direction, a log level. Using bare numbers like 2 for "closing" makes code unreadable and fragile. Enums give such values names. typedef gives any type a new name, which C programmers use to shorten struct types, to make function pointer types readable, and to hide implementation details. Both show up in essentially every real C code base.

Enumerations

#include <stdio.h>

enum color { RED, GREEN, BLUE, COLOR_COUNT };

static const char *color_names[COLOR_COUNT] = { "red", "green", "blue" };

enum http_status { HTTP_OK = 200, HTTP_NOT_FOUND = 404, HTTP_TEAPOT = 418 };

const char *describe(enum color c)
{
    switch (c) {
    case RED:   return "warm";
    case GREEN: return "natural";
    }
    return "?";
}

int main(void)
{
    for (enum color c = RED; c < COLOR_COUNT; c++)
        printf("%d %s %s\n", c, color_names[c], describe(c));
    printf("sizeof(enum color) = %zu\n", sizeof(enum color));
    enum http_status s = HTTP_NOT_FOUND;
    printf("status %d\n", s);
    enum color oops = 42;     /* compiles: enums are just ints */
    printf("oops = %d\n", oops);
    return 0;
}
0 red warm
1 green natural
2 blue ?
sizeof(enum color) = 4
status 404
oops = 42

What the declaration does:

Enums in C are not type-safe. enum color oops = 42; compiles without a warning, and you can pass an enum http_status where an enum color is expected. The names are for humans and for the compiler's switch checking, nothing more. (C++ and Rust are stricter.)

The count sentinel and lookup tables

Two idioms in the example deserve attention.

The sentinel. Adding COLOR_COUNT as the last constant gives you the number of real values for free. Use it to size arrays and bound loops; when you add a color before it, the count updates itself.

Lookup tables. Because the constants are 0, 1, 2, ..., an array indexed by the enum maps each value to data: names, sizes, handlers. color_names[c] turns a color into text. Keep the table and the enum in the same order (and next to each other in the source) so they cannot drift apart.

  enum value:     RED(0)    GREEN(1)    BLUE(2)
                    |          |           |
  color_names:  [ "red" ,  "green" ,   "blue" ]

switch and -Wswitch

switch over an enum is where enums earn their keep. Compiling the example with -Wall gives:

e1.c:11:5: warning: enumeration value 'BLUE' not handled in switch [-Wswitch]
e1.c:11:5: warning: enumeration value 'COLOR_COUNT' not handled in switch [-Wswitch]

When a switch on an enum variable has no default label, gcc checks that every constant has a case. This is extremely valuable when a program grows: add a new value to the enum and the compiler lists every switch you forgot to update. Adding a default: silences the check, so many style guides recommend no default in switches over enums and a separate return or error after the switch for impossible values. (The sentinel shows up in the warning too; handle it with a case COLOR_COUNT: that falls through to the error path.)

typedef: new names for types

typedef creates an alias for an existing type:

typedef unsigned char byte;          /* byte now means unsigned char */
typedef struct point Point;          /* Point now means struct point */

Syntactically, a typedef looks exactly like a variable declaration with typedef in front, and the name being "declared" becomes the type's new name. That rule makes even complicated typedefs readable.

The commonest use is to drop the struct keyword, often combined with the definition:

#include <stdio.h>

typedef struct {
    double x, y;
} Vec2;

typedef double (*unary_fn)(double);

Vec2 vec2_add(Vec2 a, Vec2 b)
{
    return (Vec2){ a.x + b.x, a.y + b.y };
}

double twice(double v) { return 2 * v; }
double square(double v) { return v * v; }

Vec2 vec2_map(Vec2 v, unary_fn f)
{
    return (Vec2){ f(v.x), f(v.y) };
}

int main(void)
{
    Vec2 a = { 1.5, 2.0 }, b = { 0.5, -1.0 };
    Vec2 c = vec2_add(a, b);
    printf("c = (%g, %g)\n", c.x, c.y);
    unary_fn ops[] = { twice, square };
    for (int i = 0; i < 2; i++) {
        Vec2 m = vec2_map(c, ops[i]);
        printf("op %d: (%g, %g)\n", i, m.x, m.y);
    }
    return 0;
}
c = (2, 1)
op 0: (4, 2)
op 1: (4, 1)

New pieces:

When to typedef

Opinions differ, and you will see both styles:

A naming caution: POSIX reserves type names ending in _t (size_t, pid_t, uint32_t). Naming your own types point_t is common and usually harmless, but strictly speaking those names belong to the system.

Common mistakes

Recap

Going further: look at /usr/include/x86_64-linux-gnu/bits/confname.h in the sandbox for a large real enum (the names accepted by pathconf and sysconf), and notice how each constant is followed by #define NAME NAME, a trick that lets code test for it with #ifdef (the preprocessor module explains why that is needed).

Exercises

Exercise 1 · quiz

Enum values

enum e { A, B = 5, C, D = 2, E };
printf("%d %d %d\n", A, C, E);

What does this print?

Answer
  • 0 6 3

The first constant is 0 unless given a value; each later constant is one more than the previous one unless given its own value. So A = 0, B = 5, C = 6, D = 2, E = 3. Values may repeat and may appear in any order; the compiler does not care. That flexibility is why enums are not checked types in C: an enum variable is just an int.

Exercise 2 · write code

A traffic light state machine

Model a traffic light with

enum light { RED, GREEN, YELLOW };

Write enum light next_light(enum light l) (RED goes to GREEN, GREEN to YELLOW, YELLOW to RED) using a switch with one case per value, and const char *light_name(enum light l) returning "red", "green" or "yellow" (a lookup table indexed by the enum is a good way).

main reads a number of steps n from standard input and prints the state n + 1 times, starting at RED, one per line, as step K: NAME. For input 4:

step 0: red
step 1: green
step 2: yellow
step 3: red
step 4: green

If n is missing or negative, print bad steps and exit with status 1.

File: main.c

Starting point

#include <stdio.h>

enum light { RED, GREEN, YELLOW };

enum light next_light(enum light l)
{
    return l;
}

const char *light_name(enum light l)
{
    return "?";
}

int main(void)
{
    int n;
    if (scanf("%d", &n) != 1 || n < 0) {
        printf("bad steps\n");
        return 1;
    }
    enum light l = RED;
    for (int k = 0; k <= n; k++) {
        printf("step %d: %s\n", k, light_name(l));
        l = next_light(l);
    }
    return 0;
}

Example

input

4

expected output

step 0: red
step 1: green
step 2: yellow
step 3: red
step 4: green

Example

input

0

expected output

step 0: red
Hint 1

switch (l) { case RED: return GREEN; ... } with a case for each of the three values.

Hint 2

For names: static const char *names[] = { "red", "green", "yellow" }; return names[l]; works because RED, GREEN, YELLOW are 0, 1, 2.

Reference solution
#include <stdio.h>

enum light { RED, GREEN, YELLOW };

enum light next_light(enum light l)
{
    switch (l) {
    case RED:    return GREEN;
    case GREEN:  return YELLOW;
    case YELLOW: return RED;
    }
    return RED;   /* not reached for valid values */
}

const char *light_name(enum light l)
{
    static const char *names[] = { "red", "green", "yellow" };
    return names[l];
}

int main(void)
{
    int n;
    if (scanf("%d", &n) != 1 || n < 0) {
        printf("bad steps\n");
        return 1;
    }
    enum light l = RED;
    for (int k = 0; k <= n; k++) {
        printf("step %d: %s\n", k, light_name(l));
        l = next_light(l);
    }
    return 0;
}

Exercise 3 · write code

Add a new kind of shape

The starter reads shapes from standard input, one per line, and prints each one's area. It knows circle R and square S. Add a third kind, rect W H (a rectangle with width and height):

Output for each shape: NAME area AREA with the area printed by %.2f. For

circle 1
rect 2 3.5
square 2

the output is

circle area 3.14
rect area 7.00
square area 4.00

An unknown shape prints unknown shape: WORD and exits with status 1. The check also rebuilds your code with -Werror=switch, so every enum value must be handled in the switch.

File: main.c

Starting point

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

typedef enum { CIRCLE, SQUARE } ShapeKind;

typedef struct {
    ShapeKind kind;
    double a, b;      /* circle: a = radius; square: a = side */
} Shape;

static const char *kind_names[] = { "circle", "square" };
#define NUM_KINDS (sizeof kind_names / sizeof kind_names[0])

double area(Shape s)
{
    switch (s.kind) {
    case CIRCLE: return 3.14159265358979 * s.a * s.a;
    case SQUARE: return s.a * s.a;
    }
    return 0.0;
}

/* Finds the kind named word. Returns 1 and sets *out if found, else 0. */
int parse_kind(const char *word, ShapeKind *out)
{
    for (size_t i = 0; i < NUM_KINDS; i++) {
        if (strcmp(word, kind_names[i]) == 0) {
            *out = (ShapeKind)i;
            return 1;
        }
    }
    return 0;
}

int main(void)
{
    char word[16];
    while (scanf("%15s", word) == 1) {
        Shape s = { 0 };
        if (!parse_kind(word, &s.kind)) {
            printf("unknown shape: %s\n", word);
            return 1;
        }
        if (scanf("%lf", &s.a) != 1)
            return 1;
        printf("%s area %.2f\n", kind_names[s.kind], area(s));
    }
    return 0;
}

Example

input

circle 1
rect 2 3.5
square 2

expected output

circle area 3.14
rect area 7.00
square area 4.00

Example

input

rect 10 0.5
rect 1.5 1.5

expected output

rect area 5.00
rect area 2.25
Hint 1

The enum value and the name must be at the same position: { CIRCLE, SQUARE, RECT } and { "circle", "square", "rect" }.

Hint 2

After reading s.a, read s.b too when s.kind == RECT.

Hint 3

Add case RECT: return s.a * s.b; to the switch.

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

typedef enum { CIRCLE, SQUARE, RECT } ShapeKind;

typedef struct {
    ShapeKind kind;
    double a, b;      /* circle: a = radius; square: a = side; rect: a x b */
} Shape;

static const char *kind_names[] = { "circle", "square", "rect" };
#define NUM_KINDS (sizeof kind_names / sizeof kind_names[0])

double area(Shape s)
{
    switch (s.kind) {
    case CIRCLE: return 3.14159265358979 * s.a * s.a;
    case SQUARE: return s.a * s.a;
    case RECT:   return s.a * s.b;
    }
    return 0.0;
}

/* Finds the kind named word. Returns 1 and sets *out if found, else 0. */
int parse_kind(const char *word, ShapeKind *out)
{
    for (size_t i = 0; i < NUM_KINDS; i++) {
        if (strcmp(word, kind_names[i]) == 0) {
            *out = (ShapeKind)i;
            return 1;
        }
    }
    return 0;
}

int main(void)
{
    char word[16];
    while (scanf("%15s", word) == 1) {
        Shape s = { 0 };
        if (!parse_kind(word, &s.kind)) {
            printf("unknown shape: %s\n", word);
            return 1;
        }
        if (scanf("%lf", &s.a) != 1)
            return 1;
        if (s.kind == RECT && scanf("%lf", &s.b) != 1)
            return 1;
        printf("%s area %.2f\n", kind_names[s.kind], area(s));
    }
    return 0;
}

Exercise 4 · quiz

Reading a typedef

What does this declare?

typedef int (*compare_fn)(const void *, const void *);
Answer
  • A type name compare_fn meaning "pointer to a function taking two const void pointers and returning int"

Read it as a declaration first: without typedef, int (*compare_fn)(const void *, const void *); declares a variable compare_fn that is a pointer to such a function. Putting typedef in front turns the name being declared into a type name instead. Now compare_fn cmp = by_name; declares a function pointer variable, and qsort's last parameter could be written as compare_fn. That is the trick for every typedef: write the declaration of a variable, then prefix typedef.