Strongly Typed Enumerations
Enumerations give a fixed set of integer values meaningful names, which makes code more readable and less error-prone.
Why Named Values Beat Raw Numbers
Imagine a program that tracks the state of a traffic light. One approach is to decide that 0 means red, 1 means green, and 2 means yellow, then write those numbers directly wherever a state is needed. That approach works, but a reader of the code has to remember the mapping, and nothing stops a stray 7 from sneaking in. Before reading on, pause and ask yourself: if you saw the line state = 7; in a large program, could you tell whether it was a mistake?
An enumeration offers a cleaner route. You give a small, fixed set of integer values meaningful names and then use those names everywhere. The code stops asking the reader to decode numbers and starts describing what it is doing. This article looks at why that matters, and at the stricter form of enumeration introduced by enum class, which adds a further protection against mixing up values.
Tracing a Traffic Light State
Follow the traffic light through the two approaches. With raw numbers, the program stores a single integer for the current state. Reading the value 0 tells you nothing on its own; you must remember that 0 was chosen to mean red. If a value of 7 appears, the program has no way to recognize it as outside the intended set, because 7 is still a valid integer.
- Step 1: The state is stored as the integer 0. The reader must remember that 0 means red.
- Step 2: The state is changed to 1. The reader must remember that 1 means green.
- Step 3: A stray 7 is stored. Nothing in the code flags it, because the program only sees a number.
- Step 4: With an enumeration, the same states are written as names such as red, green, and yellow. The stored value is still an integer, but the code now shows what each state means without a lookup.
The Belief That Enumerators Are Plain Integers
Many learners reach enumerations with a comfortable assumption: an enumerator is only a friendly label for a small integer, so any enumerator can stand in for any other value of the same kind. Under this belief, a value from one enumeration should be interchangeable with a value from a different enumeration. An integer should also be interchangeable with an enumerator as long as the number matches.
Both parts of this belief are exactly what the stricter form of enumeration is designed to reject. The common belief that enumerator names are interchangeable with integers or with other enumerations is the thing enum class is built to stop. Keep this in mind as you read the rules below: the stricter form does not change what the names mean to a reader, it changes what the compiler will allow you to do with them.
Reading an enum class Declaration
An enum class makes an enumeration a separate, strongly typed type with scoped enumerators. The declaration below names a new type called Color, specifies that its underlying integer type is std::uint8_t, and lists its enumerators. Each part of the declaration has a specific job, and the diagram below labels them in order.
The scoping is what makes the qualified form work. The enumerator red belongs to the scope of Color, so you read it as Color::red. The source states that qualified names such as Color::red avoid the ambiguity of unqualified names and are the accepted form in its example. Reading Color::red tells you both the enumeration and the value, while a plain red tells you only the value's name.
Tracing Five Declarations
What do you think happens?
Given enum class Color with an enumerator red, does the line Color c = 1; compile?
Reveal answer
No, because an integer cannot be quietly handed to an enumeration type.
The source explains that because each enum class is its own type, the compiler checks where its values are used, and an integer cannot be quietly handed to a variable of an enumeration type. Make your guess before reading the answer, then check whether your reasoning matched the type rule.
- Line 1 uses the qualified name Color::red, which names the enumerator within its enumeration.
- Line 2 uses the unqualified name red, which is not in scope for a scoped enumeration.
- Line 3 tries to store a plain integer in a Color variable.
- Line 4 tries to store a Color value in a Shape variable.
- Line 5 compares a Color value with a Shape value.
Finding the first rejected line
Decide which of the five lines in the program above the compiler accepts, and identify the first line where the types stop agreeing.
- 1Line 1
The value Color::red is a Color value stored in a Color variable. The types agree, and the qualified name removes any ambiguity. This line is accepted.
- 2Line 2
The name red is used without its enumeration. Because Color is a scoped enumeration, red is not in scope here, and the compiler cannot tell which red is meant. This line is rejected, and it is the first line in the program where the types stop agreeing.
- 3Line 3
The integer 1 is being given to a Color variable. Integers are not quietly converted to an enumeration type, so this line is rejected.
- 4Line 4
A Color value is being given to a Shape variable. Each enum class is its own type, so this line is rejected.
- 5Line 5
A Color value is compared with a Shape value. The two sides belong to different types, so this line is rejected.
Line 1 is accepted. Line 2 is the first rejected line, and lines 3, 4, and 5 are also rejected. In a debugging session, the first rejected line is usually where the reasoning went wrong.
Plain Enum Versus enum class
The difference between a plain enumeration and an enum class shows up when you mix values. With a plain enumeration, enumerators convert implicitly to integers, so a comparison between a Color value and a Shape value can compile. The stricter form blocks that conversion and blocks mixing between enumerations. The table below summarizes how the two forms treat the same kinds of mix.
| Situation | Plain enum | enum class |
|---|---|---|
| Comparing a Color value with a Shape value | Compiles, because both sides convert to integers | Rejected by the compiler |
| Giving a plain integer to a Color variable | Accepted through implicit conversion | Rejected, integers are not quietly converted |
| Using an unqualified enumerator name | Name is visible in the enclosing scope | Rejected unless qualified, for example Color::red |
| Giving a Color value to a Shape variable | Accepted through integer conversion | Rejected, each enum class is its own type |
How the same mixes of enumerator values are treated under each rule
Mistakes When Writing Enumerations
Common mistake Writing an unqualified enumerator name for an enum class
What it looks likeColor x = red;Why it failsThe compiler cannot tell which red is meant, so the name is rejected.
Do this insteadWrite the qualified name, such as Color x = Color::red;Common mistake Assuming an integer can stand in for an enumerator
What it looks likeColor c = 1;Why it failsIntegers are not quietly handed to an enumeration type, so the number 1 is not a Color value just because it matches an enumerator's position.
Do this insteadAssign an enumerator name such as Color::green instead of a raw number.
Common mistake Comparing values from two different enumerations
What it looks likebool same = (a == Shape::circle);Why it failsEach enum class is its own type, so a Color value and a Shape value cannot be compared as if they were interchangeable.
Do this insteadCompare values only within the same enumeration type, or convert deliberately if the design truly requires it.
Common mistake Treating enumerator names as a substitute for documentation of what the numbers mean in a storage format
Find the First Rejected Line
Read this program and identify the first line the compiler would reject.
enum class Dir { north, south };enum class Turn { left, right };
int main() {
Dir d = Dir::north; // line 1
Turn t = Dir::south; // line 2
Dir e = 0; // line 3
Turn u = Turn::left; // line 4
}Hints
- Compare the type on the left of each declaration with the type of the value on the right.
- Line 1 is a Dir value stored in a Dir variable, so it should be accepted.
- Ask whether a Dir value can be given to a Turn variable, since each enum class is its own type.
Key Takeaways
- An enumeration gives a small, fixed set of integer values meaningful names, which makes code easier to read and less error-prone than raw numbers.
- Enumerators are stored as integers, but treating them as interchangeable with integers or with other enumerations is a misconception.
- enum class makes an enumeration a separate, strongly typed type with scoped enumerators, so the compiler can check where its values are used.
- Qualified names such as Color::red avoid the ambiguity of unqualified names, and a mix of values from different enumerations or a plain integer is rejected at compile time.
- When debugging, locate the first line where the types stop agreeing, because that is where the compiler points and where the reasoning first goes wrong.