C Part I · Module 10 · Structures, Unions, Enums and User-Defined Types
Enumerations
The tagged union in the previous chapter used an int and three #defined names, and that arrangement does its job badly in three ways: nothing says the names belong to one set, the numbering is maintained by hand, and the tag accepts every integer there is. C has a construct built for exactly this — a small set of named, related integer constants with a type name attached — and it fixes the first two problems completely. The third is where this chapter earns its place, because enum looks like a checked type and is not one, and knowing precisely how much checking you get turns out to hinge on a compiler behaviour that most style guides tell you to switch off.
Intermediate18 min readCPart IFundamentalsTypes
Part I · Module 10 · Chapter 10.8 · Enumerations
1. The Declaration
enum colour { RED, GREEN, BLUE };That declares an enumerated type with tag colour and three enumeration constants. The shape is the one you know from Chapter 10.2: enum keyword, a tag in its own namespace, a braced list, a required semicolon, and the type spelled enum colour in two words.
Values are assigned automatically from zero, and you may set any of them explicitly — after which numbering continues from the one you set.
#include <stdio.h>
enum colour { RED, GREEN, BLUE }; /* 0, 1, 2 */
enum code { OK = 0, WARN = 10, ERR, FATAL = 100 }; /* ERR continues from WARN */
enum dup { ALPHA = 1, BETA = 1, GAMMA }; /* duplicates are legal */
int main(void)
{
printf("RED=%d GREEN=%d BLUE=%d\n", RED, GREEN, BLUE);
printf("OK=%d WARN=%d ERR=%d FATAL=%d\n", OK, WARN, ERR, FATAL);
printf("ALPHA=%d BETA=%d GAMMA=%d\n", ALPHA, BETA, GAMMA);
return 0;
}RED=0 GREEN=1 BLUE=2
OK=0 WARN=10 ERR=11 FATAL=100
ALPHA=1 BETA=1 GAMMA=2Three rules in that output.
Numbering continues from the last explicit value. ERR is 11, not 1 — it follows WARN = 10. This catches people out when a value is inserted into the middle of a partially-explicit list.
Duplicates are legal. ALPHA and BETA are both 1, and GAMMA is BETA + 1 = 2. Sometimes this is what you want (an alias); usually it is a mistake nobody reports.
The constants are ordinary integer constants. They can be used anywhere an integer constant expression can — as an array size, a case label, an initialiser for a static object.
Two immediate improvements over #defined numbers, which is what Chapter 10.7's tag used:
#define MSG_STATUS 0 | enum msg_kind { MSG_STATUS, ... } | |
|---|---|---|
| grouping | none — three unrelated macros | one named set |
| numbering | by hand; inserting means renumbering | automatic |
| visible to a debugger | no — the preprocessor removed it | yes, as a named constant |
| scope | none; a macro is global from its #define on | ordinary scope rules |
The debugger line is worth more than it looks. A #defined constant has no existence after preprocessing, so a debugger shows you 3. An enumeration constant is part of the program's type information, and the debugger shows you the name.
2. The Type, Precisely
This is where expectations and C part company, so the facts first.
sizeof(enum colour)=4 sizeof(int)=4
sizeof(RED) = 4
_Alignof(enum colour) = 4In C17:
- An enumeration constant has type
int. Notenum colour.REDis anintwhose value is 0, which is whysizeof REDis 4. - The enumerated type itself is implementation-defined — it is compatible with
char, a signed integer type, or an unsigned integer type, chosen by the implementation so that all the values fit. Here it came out 4 bytes; a different implementation could use 1 for a small enum. - So
enum colouris an integer type with a name, and not a distinct one.
In C23 two things change, and both are worth knowing:
- An enumeration constant has the enumerated type, not
int. - You may give the enumeration a fixed underlying type:
enum small : unsigned char { LOW, MID, HIGH }; /* C23 */Measured at -std=c23:
sizeof(enum small) = 1
sizeof(LOW) = 1One byte, for both the type and its constants. The same declaration at -std=c17 is rejected as non-standard:
warning: enumeration types with a fixed underlying type are a C23 extension
[-Wc23-extensions]This curriculum's baseline is C17 (Chapter 1.3), so the rest of this chapter assumes C17 behaviour and the fixed-underlying-type syntax is not used. If you need a guaranteed width — and that is a real need the moment a value has to match something outside your program — Chapter 14.2 is the chapter for it, and the answer there is not an enum.
3. How Little Checking You Get
Here is the part the keyword's appearance promises and C does not deliver. Four things that a checked enumerated type would reject:
#include <stdio.h>
enum colour { RED, GREEN, BLUE };
enum fruit { APPLE, PEAR, PLUM };
int main(void)
{
enum colour c = BLUE;
c = 97; /* an int that is not any enumerator */
printf("c = %d\n", c);
c = APPLE; /* a constant from a DIFFERENT enum */
printf("c = %d (assigned APPLE)\n", c);
c = c + 1; /* arithmetic */
printf("c = %d (after c + 1)\n", c);
enum colour d = (enum colour)500;
printf("d = %d\n", d);
return 0;
}c = 97
c = 0 (assigned APPLE)
c = 1 (after c + 1)
d = 500All four ran. Here is everything the compiler said at -Wall -Wextra -Wpedantic:
warning: implicit conversion from enumeration type 'enum fruit' to different
enumeration type 'enum colour' [-Wimplicit-enum-enum-cast]One warning, for one of the four. The cross-enum assignment is caught. Assigning 97, doing arithmetic, and casting 500 all passed in silence, and the variable happily held values no enumerator names.
An
enumvariable is an integer variable. It will hold any value its underlying type can represent, whether or not an enumerator names it. The enumerated type is a naming and documentation device with one static check attached (§4), not a constraint on the values.
This is a genuine difference from several other languages, and the practical consequence is concrete: a value of enumerated type arriving from outside your program — a file, a message, a caller you do not control — must be validated like any other integer. The type will not do it for you.
static int is_valid_colour(int v) { return v >= RED && v <= BLUE; }Note that such a check relies on the enumerators being contiguous, which is a property of your declaration rather than of enums. With sparse values, check each one.
4. The One Check You Do Get, and How to Lose It
There is exactly one place where an enum buys you compile-time safety, and it is worth a lot.
enum colour { RED, GREEN, BLUE };
static const char *name_of(enum colour c)
{
switch (c) {
case RED: return "red";
case GREEN: return "green";
/* BLUE is missing */
}
return "?";
}warning: enumeration value 'BLUE' not handled in switch [-Wswitch]The compiler knows every enumerator of enum colour, sees that BLUE has no case, and says so — by name. Add a fourth colour tomorrow and every switch that handles colours tells you where to go. That is a real, checked guarantee of exhaustiveness, and it is the main reason to declare an enum rather than a set of integer constants.
Now add a default case to the same switch:
switch (c) {
case RED: return "red";
case GREEN: return "green";
default: return "other";
}$ cc -std=c17 -Wall -Wextra -Wpedantic withdefault.c -o withdefault
$No warning. None. A default case means every value is handled, so there is nothing for -Wswitch to report — and the check is gone.
5. Naming and Scope
Enumeration constants live in the ordinary identifier namespace — the same one as variables and functions, not the separate tag namespace. Two consequences:
They can collide with anything. Two enums in one translation unit cannot both define ERROR. A variable named MAX and an enumerator named MAX cannot coexist in the same scope.
So prefix them. The convention that survives contact with real code is to prefix every enumerator with the set's name:
enum log_level { LOG_DEBUG, LOG_INFO, LOG_WARN, LOG_ERROR };
enum msg_kind { MSG_STATUS, MSG_ERROR, MSG_SAMPLE };LOG_ERROR and MSG_ERROR coexist; ERROR twice does not. This is the same discipline Chapter 11.4 will give for names shared across files, arriving early because enums are where collisions first bite.
An enum declared inside a block is scoped to that block, exactly as Chapter 10.2 §4 described for structure tags — and so are its constants.
6. Three Things Enums Are Good At
Naming a closed set of alternatives. Message kinds, states, log levels, directions. The set is small, known at compile time, and every member is handled somewhere.
Giving a tagged union a tag that a debugger can read. §8.
Providing integer constants with scope. An enumerator is an integer constant expression, so it can size an array or label a case — which const int cannot do in C:
enum { MAX_ENTRIES = 64 };
int table[MAX_ENTRIES]; /* fine */
const int max_entries = 64;
int other[max_entries]; /* not a constant expression in C */That untagged, single-constant form — enum { NAME = value }; — is a recognised idiom for "a compile-time integer constant with a name and a scope", and it is often better than a #define for exactly the reasons in §1's table.
7. The Count Sentinel, and Its Limit
A common and useful idiom puts a final enumerator whose value is the number of real ones:
#include <stdio.h>
enum level { LEVEL_DEBUG, LEVEL_INFO, LEVEL_WARN, LEVEL_ERROR, LEVEL_COUNT };
static const char *const level_name[LEVEL_COUNT] = {
[LEVEL_DEBUG] = "debug",
[LEVEL_INFO] = "info",
[LEVEL_WARN] = "warn",
[LEVEL_ERROR] = "error",
};
int main(void)
{
printf("LEVEL_COUNT = %d, table has %zu entries\n",
LEVEL_COUNT, sizeof level_name / sizeof level_name[0]);
for (enum level l = LEVEL_DEBUG; l < LEVEL_COUNT; l++) {
printf(" %d -> %s\n", l, level_name[l]);
}
return 0;
}LEVEL_COUNT = 4, table has 4 entries
0 -> debug
1 -> info
2 -> warn
3 -> errorLEVEL_COUNT is 4 because it follows the last real enumerator, so the table is sized by the enumeration and the loop is bounded by it. Adding a level in the middle renumbers everything correctly and the indexed initialisers follow — which is Chapter 10.6 §2's designated array index doing real work.
But the idiom is not self-maintaining, and this is the part usually left out. Add LEVEL_FATAL before the sentinel and leave the table alone:
$ cc -std=c17 -Wall -Wextra -Wpedantic levels.c -o levels
$LEVEL_COUNT = 5, table has 5 entries
4 -> (null)No diagnostic. The table silently grew to five entries, the fifth is a null pointer, and the loop printed it. There is no warning for a missing designated initialiser in an array the way there is for a structure.
The count sentinel keeps the table the right size; it does not keep it filled. Use it — it is still much better than a hand-maintained number — and pair it with a function that switches over the enum without a
default, so that §4's exhaustiveness warning covers the names even though nothing covers the table.
Two further cautions: the sentinel is itself an enumerator, so a switch that handles every level will warn about LEVEL_COUNT unless you handle or exclude it; and the idiom needs the values to be contiguous and starting at zero, so it does not combine with explicit sparse values.
8. Back to the Tagged Union
Chapter 10.7 §5's message used int and three #defines. Here it is with an enum:
enum msg_kind { MSG_STATUS, MSG_ERROR, MSG_SAMPLE };
struct message {
enum msg_kind kind;
union {
int status;
char error[12];
double sample;
} body;
};
static void print_message(const struct message *m)
{
switch (m->kind) {
case MSG_STATUS: printf("status %d\n", m->body.status); break;
case MSG_ERROR: printf("error \"%s\"\n", m->body.error); break;
case MSG_SAMPLE: printf("sample %g\n", m->body.sample); break;
}
/* no default: adding a kind makes this switch warn */
}Be precise about what changed and what did not.
Gained: the three names are one declared set; the numbering is automatic; kind's type documents what belongs in it; a debugger prints MSG_SAMPLE; and — the real prize — adding a fourth kind makes every switch over enum msg_kind warn by name, so the compiler walks you to every place that needs updating.
Not gained: m->kind = 97; still compiles with no warning, because §3. The tag is still an integer, the constructors are still the only thing keeping it honest, and Chapter 10.7 §6's inconsistent object is still writable. An enum makes the tagged union much easier to maintain; it does not make it safe. That remains a convention enforced by having one constructor per alternative.
9. What You Should Be Able to Say Now
enum tag { A, B, C };declares an enumerated type and a set of named integer constants, numbered from 0.- Numbering continues from the last explicit value — measured,
ERRwas 11 afterWARN = 10. - Duplicates are legal and unreported.
- In C17, an enumeration constant has type
int, and the enumerated type is implementation-defined. - In C23, constants have the enumerated type, and
enum E : T { ... }fixes the underlying type — measured,sizeof1 forunsigned char. At C17 that syntax warns as an extension. - An enum variable is an integer variable:
c = 97,c = c + 1and(enum colour)500all compiled silently. Only the cross-enum assignment warned. - So values arriving from outside must be validated like any other integer.
-Wswitchreports an unhandled enumerator by name — the one real static guarantee.- Adding
default:silences it. For an exhaustive switch, omitdefaultand put the fallback after the switch. - Enumerators share the ordinary identifier namespace — prefix them with the set's name.
enum { NAME = v };gives a scoped integer constant expression, whichconst intdoes not in C.- The count sentinel sizes a table but does not fill it — measured, a missing entry became a silent null with no diagnostic.
10. Exercises
Exercise 1 — give the values. Without compiling.
enum a { P, Q, R };
enum b { S = 5, T, U };
enum c { V = 1, W = 1, X };
enum d { Y = -2, Z, AA = 10, AB };Exercise 2 — predict the diagnostics. For each, say whether -Wall -Wextra -Wpedantic warns, using §3's measurements.
enum colour c = 97;
enum colour d = APPLE; /* from enum fruit */
enum colour e = (enum colour)97;
int n = BLUE;
c = c + 1;Exercise 3 — restore the check. This switch handles every kind and still gets no exhaustiveness warning. Fix it without losing the run-time fallback, and say what you gain.
switch (m->kind) {
case MSG_STATUS: ...; break;
case MSG_ERROR: ...; break;
case MSG_SAMPLE: ...; break;
default: log("unknown"); break;
}Exercise 4 — validate the input. A message kind arrives as an int read from a file. Write the validation, say why the enumerated type does not do it for you, and say what your check assumes about the declaration.
Exercise 5 — name the set. Two modules each want an enumerator called ERROR. Say what happens, why it is not a tag-namespace question, and give the convention that fixes it.
Exercise 6 — the sentinel's limit. Explain why adding an enumerator before LEVEL_COUNT and forgetting the table entry produced no diagnostic, what the run printed, and give one change that makes the omission detectable.
Exercise 7 — enum or #define. For each, choose and justify.
- the three states of a connection
- the fixed size of a lookup table used as an array bound
- a value that must be exactly 16 bits wide to match a message field
- a debug flag switched on and off while building
Exercise 8 — the C23 question. You are asked to use enum state : unsigned char to make a structure smaller. Say what the C17 compiler reports, what you would check before adopting it, and which chapter covers the alternative when a width has to be guaranteed.
11. Summary
enumdeclares a named set of integer constants, numbered from 0, continuing from the last explicit value; duplicates are legal.- C17: constants are
int, the enumerated type is implementation-defined. C23: constants take the enumerated type, andenum E : Tfixes the width. - An enum variable is an integer variable —
97, arithmetic and a cast all passed silently; only the cross-enum assignment warned. - Validate values from outside; the type will not.
-Wswitchnames an unhandled enumerator — the one real static check — anddefault:turns it off.- For an exhaustive switch, omit
defaultand put the fallback after it. - Prefix enumerators; they share the ordinary identifier namespace.
enum { NAME = v };is a scoped integer constant expression.- The count sentinel sizes a table but does not fill it — a missing entry was a silent null.
- On a tagged union an enum buys maintainability and the switch warning, not safety.
Every type in this module has been spelled in two words. struct reading, union payload, enum msg_kind — the keyword and the tag, everywhere, in every declaration, every parameter list and every cast. It is tiresome, and C has a facility that gives a type a one-word name of your choosing, which is why a great deal of real code never writes struct at all. That facility is also responsible for some of the least readable C ever written, because a name that hides what a type is costs the reader more than the two words saved — and the worst version of that is a name that hides a pointer, so that a declaration which looks like an object is really an address and the reader has no way to tell. The next chapter is about which of those two outcomes you get, and the rule that separates them.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
