C++ Part I · Module 9 · Arrays, Strings, and Aggregate Data
Enumerations and enum class
Two chapters have now pointed at this construct and left it out. Chapter 9.7 built a register type and declined to give it an access mode, on the grounds that read-only, read-write and write-one-to-clear are a small set of named values and calling them an int throws away the only interesting thing about them. Chapter 9.8 then showed what happens when two members share a type: the brace initializer accepts them in either order, silently, because positional matching has nothing else to go on. Both problems want the same answer, which is a type whose values are a fixed named set rather than a range of numbers. C++ has that, and it has it twice. The two forms are written one word apart and differ in exactly one respect — whether a value of the type will quietly turn into an integer when the surrounding code asks for one. That sounds like a technicality and it is the whole difference between a type that catches a mistake and one that hides it, which is why the frozen curriculum states a preference rather than presenting two equal options. This chapter measures three ordinary-looking operations that one form allows and the other refuses.
Foundation16 min readC++Part IFundamentalsEnums
Part I · Module 9 · Chapter 9.9 · Enumerations and enum class
1. The Member Two Chapters Left Out
Chapter 9.7 §3 built a register type and stopped short:
"A register's access mode — read-only, read-write, write-one-to-clear — is naturally a small set of named values, and representing it as an
intthrows that away. Chapter 9.9 is the type for it, and this chapter leaves the member out rather than modelling it badly."
And Chapter 9.8 §4 named the other half of the problem: two members of the same type are interchangeable to a brace initializer, so Field{4, 8} and Field{8, 4} are both accepted and only one is right.
An enumeration answers both. It is a type whose values are a fixed set you name:
enum class Access { ReadOnly, ReadWrite, WriteOneToClear };An enumeration is a type with a small, fixed set of named values.
2. What the Alternative Throws Away
Without one, the usual approach is named integer constants:
const int ReadOnly = 0;
const int ReadWrite = 1;
const int WriteOneToClear = 2;
int mode = ReadWrite;That gives you the names and nothing else. Four things are missing, and all four matter:
There is no type. mode is an int, so mode = 47 is legal and means nothing.
Nothing groups the three. They are three unrelated constants that happen to be declared together.
A function taking a mode takes an int, so it accepts any integer — and Chapter 9.8 §4's swap problem applies in full.
And a switch over it cannot be checked. Chapter 5.3 §9's callout was explicit about this: for an int there is nothing to compare the case list against, because any of four billion values is possible.
An enumeration fixes all four at once by being a type. §8 collects the switch payoff that Chapter 5.3 promised.
3. Two Forms, One Word Apart
enum PlainAccess { ReadOnly, ReadWrite, WriteOneToClear }; // unscoped, inherited from C
enum class Access { ReadOnly, ReadWrite, WriteOneToClear }; // scoped, C++11Two visible differences and one that matters.
Qualification. A scoped enum's values live inside it, so you write Access::ReadWrite. A plain enum's values spill into the surrounding scope, so you write ReadWrite — which also means two plain enums cannot both have a ReadOnly.
The one that matters: whether a value converts to an integer on its own. §4 measures it.
And the register from Chapter 9.7, completed:
enum class Access { ReadOnly, ReadWrite, WriteOneToClear };
struct Register {
int address;
int width;
Access access; // no longer an int pretending to be something
};Register{0x40, 32, Access::ReadWrite} now has one member that cannot be confused with the other two, which is Chapter 9.8 §4's advice taken.
4. The One Difference, Measured
#include <iostream>
enum PlainAccess { ReadOnly, ReadWrite, WriteOneToClear };
enum class Access { ReadOnly, ReadWrite, WriteOneToClear };
int main()
{
PlainAccess p = ReadWrite;
Access a = Access::ReadWrite;
std::cout << "a plain enumerator converts to int silently: " << p + 0 << '\n';
std::cout << "...and prints as a number: " << p << '\n';
std::cout << "a scoped enumerator needs a cast: "
<< static_cast<int>(a) << '\n';
std::cout << "underlying type sizes: PlainAccess " << sizeof(PlainAccess)
<< " Access " << sizeof(Access) << '\n';
return 0;
}Measured output, zero warnings:
a plain enumerator converts to int silently: 1
...and prints as a number: 1
a scoped enumerator needs a cast: 1
underlying type sizes: PlainAccess 4 Access 4Both are 4 bytes and both represent the same three values. The difference is entirely in what the type permits, and three ordinary operations separate them:
| Operation | plain enum | enum class |
|---|---|---|
int n = mode; | compiles | error: cannot initialize a variable of type 'int' with an lvalue of type 'Access' |
if (mode == 1) | compiles | error: invalid operands to binary expression ('Access' and 'int') |
std::cout << mode; | compiles, prints a number | error: invalid operands to binary expression ('ostream' … and 'Access') |
The plain enum allows all three. The scoped enum refuses all three.
That is the frozen purpose's sentence, measured: the difference between a type that catches a mistake and one that hides it.
5. Why Each Refusal Is Worth Having
Taking them in order, because none of the three is obviously a mistake until you see what it permits.
int n = mode; — once a mode is an int, every integer operation is available to it. n + 1, n * 2, n compared against an unrelated count. None of those means anything for an access mode, and the plain enum has no way to object.
if (mode == 1) — this is the one that rots. The 1 is ReadWrite today, because the enumerators happen to start at 0 and ReadWrite happens to be second. Insert a new value at the front and every such comparison silently means something different. With a scoped enum the comparison does not compile, so the question never arises.
std::cout << mode; — printing a number where a name was wanted. The plain enum prints 1; the scoped enum refuses, which forces you to write the conversion you wanted:
std::cout << static_cast<int>(a); // deliberate: print the number
std::cout << to_string(a); // or write a function returning a nameChapter 4.10 §4's argument applies exactly: the cast makes the conversion visible, and a reader can see that a number was intended rather than a name.
The scoped enum does not make these operations impossible — it makes them explicit.
static_cast<int>(a)is always available. What is removed is the possibility of doing it by accident.
6. When You Actually Want the Conversion
Two cases, both legitimate, and both better with the cast than without the type.
Indexing a table. A mode often maps to a row:
const char* const names[] = {"ReadOnly", "ReadWrite", "WriteOneToClear"};
std::cout << names[static_cast<int>(a)];That works and it is fragile in a way worth noticing: it depends on the enumerators' numeric values matching the array's order, which nothing checks. A function with a switch is longer and does not have that dependency — and Chapter 5.3's -Wswitch will tell you when a new enumerator is added, which the array will not.
A hardware or protocol value. When the numbers are specified externally, say so:
enum class Access : std::uint8_t { // the underlying type, chosen
ReadOnly = 0x0,
ReadWrite = 0x1,
WriteOneToClear = 0x3, // values from a specification
};Two things there. Enumerators can be given explicit values, which is required when they are not yours to choose. And the underlying type can be specified — §7.
7. The Underlying Type
An enumeration's values are stored as some integer type. Measured above: both forms were 4 bytes by default here.
For a scoped enum you can state it, with the : type syntax above. Two reasons to:
Size, when it matters. enum class Access : std::uint8_t is one byte rather than four. In a struct that is part of an array of a million, or that must match a specified byte layout, that is the difference between Chapter 9.8's Poor and Better.
Range, when the values are specified. If a field is documented as 8 bits, saying std::uint8_t records that.
For a plain enum without an explicit type the choice is implementation-defined within limits, which is one more reason to prefer the scoped form where you care.
There is a library facility for recovering the underlying type generically, std::underlying_type, which belongs with the template material of Module 18 and is named here only so you know the question has an answer.
8. The switch Payoff Chapter 5.3 Promised
Chapter 5.3 §9's callout said this explicitly:
"For an enumeration there is: the type lists its own valid values… a
switchover one gets a compiler check that noifchain can ever get — a diagnostic when you handle three of four cases and forget the fourth. That check is the reason to preferswitchfor enumerated state."
That is now collectable. Chapter 5.3 measured both halves:
An unhandled enumerator warns — -Wswitch, at the standing flags.
And adding a default: silences it. Measured there: zero warnings once a default was present.
Which produces a rule that runs opposite to Chapter 5.3 §3's advice for an int switch:
| Switching on | Include a default? |
|---|---|
an int | yes — an unexpected value is possible and means something went wrong |
| an enumeration | no — omit it, so that adding an enumerator produces a warning |
For an enumeration, the default is the thing that throws away the check. Leaving it out means the compiler tells you every place that needs updating when the set of values grows, which is exactly the maintenance property you want from naming a fixed set in the first place.
9. What to Actually Do
Use enum class. The plain form exists for C compatibility and for code you did not write.
Give it an underlying type when the size matters or the values are externally specified — enum class Access : std::uint8_t.
Give enumerators explicit values only when they are specified elsewhere; otherwise let them be 0, 1, 2.
Cast explicitly when you want the number — static_cast<int>(a). It is available and it is visible.
Switch on an enumeration and omit the default, so -Wswitch reports an unhandled case. The opposite of the rule for an int.
Prefer a switch to a table indexed by the enumerator — the table silently depends on the numeric values.
Use one as a struct member wherever a field is a fixed set of choices. It solves Chapter 9.8 §4's swap problem for that member.
One construct deliberately not covered: a set of bit flags that are combined with | and tested with & — Chapter 4.8's operators. A scoped enum makes that awkward on purpose, because combining two enumerators produces a value that is not any enumerator. The usual answers are a plain enum for the constants, an integer with named masks, or operator overloads written deliberately (Module 15). It is a real exception to §9's first rule and worth recognising as one rather than fighting the type.
10. Mistakes Worth Making Once
Using named int constants for a fixed set. No type, no grouping, no switch check.
Using a plain enum in new code. Measured: it permits all three of §4's operations.
Writing if (mode == 1). It depends on an enumerator's numeric value, which can change when a value is inserted.
Printing a scoped enum directly. It is refused; write the cast or a name function.
Including a default: in a switch over an enumeration. It silences the one check the type was for.
Indexing a table with static_cast<int>(a). It depends on the numeric values matching the table order, and nothing checks it.
Expecting the default underlying type to be a particular size. Measured 4 here; state it with : std::uint8_t when it matters.
Trying to OR two scoped enumerators. The type refuses by design; §9 names the three alternatives.
11. Interview Relevance
"What is the difference between enum and enum class?" Two things: scoping of the enumerators, and — the one that matters — whether the values implicitly convert to an integer. The strong answer gives a concrete refusal, such as int n = a; failing or a == 1 failing.
"Why prefer enum class?" Because the implicit conversion is what lets a mistake through: comparing against a literal, printing a number where a name was wanted, mixing a mode with a count. Adding that static_cast is still available — so nothing is lost, only made explicit — is the complete answer.
"Should a switch over an enum have a default?" No, and the reason is the discriminator: omitting it lets -Wswitch tell you about every unhandled enumerator when a new one is added. This is the opposite of the advice for an int.
"How do you control the size of an enum?" enum class Access : std::uint8_t. Worth knowing when a type is a member of something large or has to match a specified layout.
12. What You Should Be Able to Say Now
- An enumeration is a type with a small, fixed set of named values — the answer to both Chapter 9.7 §3's and Chapter 9.8 §4's problems.
- Named
intconstants give the names and nothing else: no type, no grouping, anintparameter, and noswitchcheck. - The two forms differ in scoping and — the one that matters — implicit conversion to an integer.
- Measured: both are 4 bytes and represent the same three values.
- Measured:
int n = p;compiles andint n = a;is an error;p == 1compiles anda == 1is an error;std::cout << pprints 1 andstd::cout << ais an error. - The plain enum permits all three; the scoped enum refuses all three.
if (mode == 1)is the one that rots — it depends on a numeric value that changes when an enumerator is inserted.- The scoped form makes these explicit rather than impossible —
static_cast<int>(a)is always available. - Enumerators can be given explicit values, which is right when they come from a specification.
- The underlying type can be stated —
enum class Access : std::uint8_t— for size or for a specified range. - Chapter 5.3's payoff:
-Wswitchreports an unhandled enumerator, and adefault:silences it. - So omit
defaultfor an enumeration and include it for anint— the rules run opposite. - Prefer a
switchto a table indexed by the enumerator, which silently depends on the numeric values. - A type is a restriction and the restriction is the product — the same shape as
std::string,std::string_viewandstructin this module.
13. Exercises
Exercise 1 — which form? Rewrite const int ReadOnly = 0; const int ReadWrite = 1; as a scoped enumeration, and list the four things the enumeration gives you that the constants do not.
Exercise 2 — predict the diagnostic. With enum P { A, B }; and enum class S { A, B };, say which compile and give the error otherwise: (a) int n = B; (b) int n = S::B; (c) if (B == 1) (d) if (S::B == 1) (e) std::cout << B; (f) std::cout << static_cast<int>(S::B);.
Exercise 3 — why it rots. Explain what happens to every if (mode == 1) in a codebase when a new enumerator is inserted at the front of a plain enum, and why the scoped form prevents it.
Exercise 4 — the default rule. For each switch, say whether to include a default: and why: (a) on an int opcode (b) on an enum class Access (c) on a char command (d) on an enum class whose values come from a hardware specification.
Exercise 5 — size it. Give the declaration for an access mode that must occupy exactly one byte and whose values are 0, 1 and 3 as specified by a datasheet.
Exercise 6 — table or switch? You need a name string for each enumerator. Give both implementations, and say what the table version depends on that the switch version does not.
Exercise 7 — complete the register. Take Chapter 9.7's struct Reg { int address; Field field; };, add an access mode and a width, order the members per Chapter 9.8 §8, and say which of Chapter 9.8 §4's hazards your version has removed.
14. Summary
- An enumeration is a type with a small, fixed set of named values — and it answers both problems Chapters 9.7 and 9.8 left open.
- Named
intconstants give names only: no type, no grouping, noswitchcheck. - The forms differ in scoping and in implicit conversion to an integer.
- Measured: both 4 bytes; the plain form permits
int n = p,p == 1andstd::cout << p, and the scoped form refuses all three. if (mode == 1)depends on a numeric value that can change.- The scoped form makes conversions explicit, not impossible —
static_cast<int>is there. - State the underlying type when size or a specification matters.
- Chapter 5.3's payoff:
-Wswitchcatches an unhandled enumerator, anddefault:silences it — so omitdefaultfor enums. - Bit flags are the one real exception, and a scoped enum makes them awkward deliberately.
- A type is a restriction, and the restriction is the product.
Chapter 9.7 said a struct lets you reach its members at any depth, and every example since has done so one dot at a time — r.field.width, p->field.offset. That is correct and it gets verbose at the point of use, particularly for a function that returns a group of two or three values and a caller that wants all of them. The next chapter is a C++17 form that takes a struct apart into named variables in a single declaration, so a function returning a value and a status reads as two named things rather than as one object you keep indexing into. It carries the same copy-versus-reference decision Chapter 5.6 measured for loop variables, with the same silent failure if you get it wrong.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 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.
