Skip to content
VLSI Mentor

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 int throws 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    enum       PlainAccess { ReadOnly, ReadWrite, WriteOneToClear };   // unscoped, inherited from C
    enum class Access      { ReadOnly, ReadWrite, WriteOneToClear };   // scoped, C++11

Two 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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

Azvya Education Pvt. Ltd.VLSI Mentor
enums.cpp — complete program
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 4

Both 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:

Operationplain enumenum class
int n = mode;compileserror: cannot initialize a variable of type 'int' with an lvalue of type 'Access'
if (mode == 1)compileserror: invalid operands to binary expression ('Access' and 'int')
std::cout << mode;compiles, prints a numbererror: 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    std::cout << static_cast<int>(a);            // deliberate: print the number
    std::cout << to_string(a);                   // or write a function returning a name

Chapter 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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 switch over one gets a compiler check that no if chain can ever get — a diagnostic when you handle three of four cases and forget the fourth. That check is the reason to prefer switch for 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 onInclude a default?
an intyes — an unexpected value is possible and means something went wrong
an enumerationno — 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

  1. 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.
  2. Named int constants give the names and nothing else: no type, no grouping, an int parameter, and no switch check.
  3. The two forms differ in scoping and — the one that matters — implicit conversion to an integer.
  4. Measured: both are 4 bytes and represent the same three values.
  5. Measured: int n = p; compiles and int n = a; is an error; p == 1 compiles and a == 1 is an error; std::cout << p prints 1 and std::cout << a is an error.
  6. The plain enum permits all three; the scoped enum refuses all three.
  7. if (mode == 1) is the one that rots — it depends on a numeric value that changes when an enumerator is inserted.
  8. The scoped form makes these explicit rather than impossible — static_cast<int>(a) is always available.
  9. Enumerators can be given explicit values, which is right when they come from a specification.
  10. The underlying type can be stated — enum class Access : std::uint8_t — for size or for a specified range.
  11. Chapter 5.3's payoff: -Wswitch reports an unhandled enumerator, and a default: silences it.
  12. So omit default for an enumeration and include it for an int — the rules run opposite.
  13. Prefer a switch to a table indexed by the enumerator, which silently depends on the numeric values.
  14. A type is a restriction and the restriction is the product — the same shape as std::string, std::string_view and struct in 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 int constants give names only: no type, no grouping, no switch check.
  • The forms differ in scoping and in implicit conversion to an integer.
  • Measured: both 4 bytes; the plain form permits int n = p, p == 1 and std::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: -Wswitch catches an unhandled enumerator, and default: silences it — so omit default for 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.