Enums and Unions
Name your program's states with enum and switch over them exhaustively, then meet union as members sharing the same bytes and the obligation to track which member is live.
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 isenum 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 noenum classand no scoped enumeration, so an enum is namedintconstants plus a type that converts to an integer freely. - Enumerators share the enclosing scope, so prefix them:
DIRECTION_NORTH, neverNORTH, or the second enum that wants that name iserror: redeclaration of enumerator 'NORTH'. Prefer an enum to#definefor 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 whereconst intis not:static int table[capacity];iserror: storage size of 'table' isn't constant.switchover an enum warnsenumeration value 'X' not handledonly while there is nodefault, but always write thedefaultanyway, 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.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Enums and Unions - Quiz
Test your understanding of the lesson.
Practice Exercises
A Reading That Knows What It Is
Read a stream of sensor lines, each a letter and a number, and print every one of them in the form its kind calls for. The type is the lesson's payoff, a tagged union: struct Reading holds an enum ReadingKind called kind next to an unnamed union member called value, so count and celsius share one set of bytes and the tag is the only record of which of them was written. main is given to you and does the reading, looping on scanf(" %c %lf", &letter, &amount) == 2, zeroing a fresh struct Reading each time round, and printing "no readings" when the input was empty. What is missing is the two functions that make the pattern work, and they are the two halves of one obligation. set_reading takes a struct Reading * and fills it in: 'c' means a count, so kind becomes READING_COUNT and value.count takes (int)amount, since the number arrived as a double and a count is an int; 't' means a temperature, so kind becomes READING_CELSIUS and value.celsius takes amount unchanged; any other letter is READING_UNKNOWN, which needs no member written at all because nothing will be read back. Members are reached with the arrow throughout, because what the function holds is an address rather than a struct, and the rule to hold on to is that the tag and the member are written together, in the same branch, never one without the other. print_reading is the other half and it must trust the tag rather than guess: it takes a const struct Reading * because it only reads, switches on reading->kind, prints "%d items\n" for READING_COUNT and "%.1f C\n" for READING_CELSIUS, and prints "unknown reading" from the default. Keep that default even though every enumerator has a case. It is what catches READING_UNKNOWN here, and it is what would catch an integer that was never in the set at all, which the compiler is under no obligation to diagnose. Reading value.count when the tag says READING_CELSIUS is not a crash and not a diagnostic, it is a number that looks plausible and is not true, so the switch is the only thing standing between the union and a lie. main returns 0 on every path including the empty one, since the checker treats a nonzero exit status as a failure however right the output looks.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!