The State That Was Just a Number

A program that tracks which way something is facing has to keep that fact somewhere, and the first answer everyone reaches for is an int. int heading = 2; compiles, costs four bytes and tells a reader nothing at all: 2 means south only inside the head of whoever typed it, heading = 9; is as legal as any other assignment, and no line anywhere says the set of headings was ever meant to close at four. An enum is C's way of saying it out loud. enum Direction { DIRECTION_NORTH, DIRECTION_EAST, DIRECTION_SOUTH, DIRECTION_WEST }; defines a type whose values have names, and like a struct definition it reserves no memory and needs the semicolon after the closing brace.

#include <stdio.h>

enum Direction
{
    DIRECTION_NORTH,
    DIRECTION_EAST,
    DIRECTION_SOUTH,
    DIRECTION_WEST
};

enum { CAPACITY = 8 };

const char *direction_name(enum Direction heading)
{
    switch (heading)
    {
        case DIRECTION_NORTH:
            return "north";
        case DIRECTION_EAST:
            return "east";
        case DIRECTION_SOUTH:
            return "south";
        case DIRECTION_WEST:
            return "west";
        default:
            return "off the compass";
    }
}

int main(void)
{
    enum Direction trail[CAPACITY] = {DIRECTION_SOUTH, DIRECTION_WEST};
    enum Direction rogue = 42;

    printf("north %d, east %d, south %d, west %d, and one constant is %zu bytes\n", DIRECTION_NORTH, DIRECTION_EAST, DIRECTION_SOUTH, DIRECTION_WEST, sizeof DIRECTION_NORTH);
    printf("%zu slots, starting %s, turning %s, then reading %s\n", sizeof trail / sizeof trail[0], direction_name(trail[0]), direction_name(trail[1]), direction_name(trail[2]));
    printf("rogue holds %d, which is %s\n", rogue, direction_name(rogue));

    return 0;
}
north 0, east 1, south 2, west 3, and one constant is 4 bytes
8 slots, starting south, turning west, then reading north
rogue holds 42, which is off the compass

The first output line is the numbering, and there is nothing clever about it: enumerators count up from 0 in the order you wrote them, so DIRECTION_WEST is 3 because it is fourth. You can set a value explicitly and the counting resumes from there, which is how error codes are usually written: in enum Status { STATUS_OK = 200, STATUS_NOT_FOUND = 404, STATUS_GONE }; the last constant is 405. The type is enum Direction, and the bare tag Direction is not a type name, which is lesson 1's rule with a different keyword in front of it and a typedef as the only thing that shortens it. The second output line pays off a smaller detail: {DIRECTION_SOUTH, DIRECTION_WEST} fills two of the eight slots and chapter 4 zeroes the rest, and zero is DIRECTION_NORTH, so the slots nobody filled read as north rather than as nothing, which is the reason to make the first enumerator a sensible default or an explicit "none". Then the part a C++ programmer has to unlearn: an enum constant has type int, measured at 4 bytes on that same first line and printed under %d because that is genuinely what it is. C has no scoped enumeration, no enum class, no Direction::North and no type that refuses to convert to an integer, so a C enum is named int constants plus a variable type the compiler is content to fill with any integer at all, which the last output line is already hinting at.

Prefix Every Constant

Those constants are not tucked inside the enum, and this is the first thing that surprises people. The enumerators land in the scope that encloses the enum, so a NORTH declared inside enum Direction at file scope is a file scope name, competing with every other name there. Two enums that each want a plain NORTH do not coexist:

compass.c:14:5: error: redeclaration of enumerator 'NORTH'
compass.c:5:5: note: previous definition of 'NORTH' with type 'enum Direction'

The note is the useful half, since it names the enum the first NORTH came from and turns a puzzling error into an obvious one. The fix is a convention rather than a language feature: prefix every enumerator with the name of its enum, DIRECTION_NORTH rather than NORTH, in UPPER_SNAKE_CASE as constants have been all course. It costs a few characters at every use and it is what real C does, because the alternative is a collision that arrives on the day someone adds a second enum to a header you already include.

The default That Stays Anyway

direction_name is chapter 2's switch with an enum in it, and none of chapter 2's rules bend: a case is a label rather than a box, every arm still needs a break or a deliberate fallthrough (return ends the arm here, so no break is required), and the labels must be integer constant expressions, which enumerators are. What is new is that gcc knows how many values the set has. Take the default out and drop one case, and it says so:

heading.c:13:5: warning: enumeration value 'DIRECTION_WEST' not handled in switch [-Wswitch]

That warning is a real gift, since it finds the case you forgot to add on the day you add a fifth direction, and it comes with a catch worth stating plainly: -Wswitch only checks exhaustiveness while the switch has no default, because a default handles everything by definition and there is nothing left to complain about. So the two protections are in tension, and this course takes a side. Always write the default. The exhaustiveness warning guards a mistake you make while editing, and the default guards a value that was never in the set to begin with, which is the risk that is actually live: enum Direction rogue = 42; compiled without a word of complaint under -Wall -Wextra, and the last output line is that 42 travelling into direction_name and leaving through the default. An enum is documentation, not a guarantee, and any integer can be assigned to an enum variable without the compiler being obliged to say anything.

The Constant That const Could Not Be

Chapter 2 ended const with a caveat and an IOU: a const object in C is one you are forbidden to assign to, not a value known at compile time, so a few contexts later in the course would refuse it. This is one of them, and the enum is the answer. enum { CAPACITY = 8 }; is an enum with no tag, declared purely for the constant inside it, and enum Direction trail[CAPACITY] is an array whose size had to be an integer constant expression and got one. Write the same thing with const int and the program stops compiling:

capacity.c:6:16: error: storage size of 'table' isn't constant
    6 |     static int table[capacity];
      |                ^~~~~

"Storage size isn't constant" is the compiler declining to size an object from a value it treats as a run time thing, and there is a trap next door: written without static, int table[capacity]; compiles silently, because C17 has variable length arrays and gcc quietly gives you one. The failure only surfaces where a VLA cannot go, which makes the rule easier to remember than the diagnostics: enum { NAME = value }; is the idiomatic C spelling of a compile-time integer constant, and it is what you reach for when a size, a limit or a case label needs a name. It is also the answer to #define, which can produce a constant expression too and gives up more than it needs to: enumerators are typed, they respect scope like any other declaration, and a debugger shows you CAPACITY where a macro has been erased by the preprocessor before the compiler ever sees the name.

Two Names for One Set of Bytes

A union is declared exactly like a struct with one word changed, union Value { int count; double measure; };, and union Value is the type by the same tag rule again. What changes is the layout, and it is the whole idea. A struct gives every member bytes of its own and is as large as all of them together with padding; a union gives every member the same bytes and is as large as its largest member. It therefore holds one member at a time rather than several values at once, and writing measure overwrites whatever count was. That leads to the rule to carry away: reading a member other than the one written last is not portable. C permits it where C++ does not, but what comes back is the bytes you stored reinterpreted through another type, which depends on the representation the implementation happens to use. Treat that read as implementation defined, and never build anything on it. Which leaves a union unable to answer the one question that matters about it, namely which member is live, so you answer it yourself. The pattern is a struct holding an enum next to the union, and it is what C uses wherever another language would reach for a variant type: the tag records which member was written, and every read switches on the tag first.

#include <stdio.h>

enum ReadingKind
{
    READING_COUNT,
    READING_CELSIUS
};

struct Reading
{
    enum ReadingKind kind;
    union
    {
        int count;
        double celsius;
    } value;
};

void print_reading(const struct Reading *reading)
{
    switch (reading->kind)
    {
        case READING_COUNT:
            printf("%d items\n", reading->value.count);
            break;
        case READING_CELSIUS:
            printf("%.1f C\n", reading->value.celsius);
            break;
        default:
            printf("unknown reading\n");
            break;
    }
}

int main(void)
{
    struct Reading basket = {.kind = READING_COUNT, .value.count = 12};
    struct Reading probe = {.kind = READING_CELSIUS, .value.celsius = 36.6};
    struct Reading broken = {.kind = 9, .value.count = 0};

    printf("int %zu, double %zu, the union %zu, a Reading %zu, count at %p, celsius at %p\n", sizeof(int), sizeof(double), sizeof basket.value, sizeof basket, (void *)&basket.value.count, (void *)&basket.value.celsius);

    print_reading(&basket);
    print_reading(&probe);
    print_reading(&broken);

    basket.value.celsius = 36.6;
    print_reading(&basket);

    return 0;
}
int 4, double 8, the union 8, a Reading 16, count at 0xfbffb61f0028, celsius at 0xfbffb61f0028
12 items
36.6 C
unknown reading
-858993459 items

Read the first output line as the union's whole specification. It is 8 bytes, the size of its largest member rather than the 12 the two members add up to, and both member names print the same address because they are two ways of spelling the same bytes. A struct Reading is 16: four for kind, four of the padding lesson 1 described, and eight for the union. The union { ... } value; written inline gives the member a name, which is why every access reads reading->value.count, one dot for the union member and another for the member inside it. print_reading then takes const struct Reading *, the shape lesson 2 made the default for a function that only reads, and the broken reading with its kind of 9 is the previous section's point restated: a tag is an integer like any other, so the default earns its place here too. Which leaves the last line, and it is the obligation. basket.value.celsius = 36.6; wrote a double into bytes whose tag still said READING_COUNT, so print_reading did exactly as it was told, read count, and reported the low half of a double's representation as a number of items. Nothing was undefined and nothing was diagnosed; the program simply lied, at run time, in a plausible format. The tag is your record of which member is live, and C checks nothing. Write the member and the tag together, in one small function per kind if the type is used widely, and never let a line assign to a union member without the assignment to the tag standing next to it.

Key Takeaways

  • enum Direction { DIRECTION_NORTH, ... }; names a set of values. The type is enum Direction, the bare tag is not a type name, and enumerators count up from 0 unless given explicit values, after which counting resumes from the last one.
  • Enum constants have type int. C has no enum class and no scoped enumeration, so an enum is named int constants plus a type that converts to an integer freely.
  • Enumerators share the enclosing scope, so prefix them: DIRECTION_NORTH, never NORTH, or the second enum that wants that name is error: redeclaration of enumerator 'NORTH'. Prefer an enum to #define for related constants, because enumerators are typed, scoped and visible in a debugger.
  • enum { CAPACITY = 8 }; is C's compile-time integer constant, usable as an array size where const int is not: static int table[capacity]; is error: storage size of 'table' isn't constant.
  • switch over an enum warns enumeration value 'X' not handled only while there is no default, but always write the default anyway, because any integer can be stored in an enum variable and no diagnostic is required.
  • A union's members share one set of bytes and its size is that of its largest member. Reading a member other than the one last written is implementation defined, never a technique. Pair the union with an enum tag in a struct and switch on the tag, because nothing else records which member is live.