TRT
Sign in

Foundations · Module 4 · Lesson 4 of 9

Enumerations: enum and enum class

Name a small set of values with an enumeration, see that it is an integer underneath, and learn why enum class avoids name clashes and accidental conversions and how switch warnings keep your code up to date.

FreeAbout 7 minutes

By the end you can

  • Declare and use a scoped enumeration and say what values it holds
  • Explain what enum class forbids that a plain enum allows
  • Convert between enumerators and integers explicitly and predict the result
  • Use a switch over an enum so that the compiler helps when enumerators change

Builds on Structs: Grouping Related Data, Conditions and Branches: if, else and switch.

01 · A question first

A status code of 2: does it mean ok, failed or pending? Bare integers carry no meaning, accept any value, and hide typos. How does an enumeration give names to a handful of values, why is a scoped one safer, and what is it in memory underneath?

Make a guess, then step through the trace below to check it.

02 · Watch it run

An enumeration is a named integerstep 1 of 5
enum class Color { Red, Green, Blue };Color c = Color::Blue;int n = static_cast<int>(c);Color d = static_cast<Color>(1);int bad = c;
  1. item 0Red: 0
  2. item 1Green: 1
  3. item 2Blue: 2
  4. item 3c: --
  5. item 4n: --
  6. item 5d: --

The enumeration defines a new type with three named values. Underneath they are integers: Red is 0, Green is 1 and Blue is 2, counted up from zero. The type is stored in 4 bytes by default, like an int. The first three cells are the enumerators, and the last three will show the variables.

Machine work: compile time: Red is 0, Green is 1, Blue is 2; 4 bytes per value

03 · The idea

Start with the problem

Programs are full of values that are one of a few choices: a direction, a color, a state, an error code. The quickest way to store them is as integers, and that is the problem. What does status = 2 mean? Nothing in the code says. Nothing stops status = 7, which is not a valid choice, or a typo that stores the wrong one. Named constants such as const int Red = 0; help the reader, but they have no type, so nothing stops you passing a direction where a color is expected.

What you want is a type with a small set of named values, so that the compiler can check them. That is an enumeration.

How the idea works

Declaring and using. The recommended form is the scoped enumeration, written enum class:

cpp
enum class Color { Red, Green, Blue };
 
Color c = Color::Green;
if (c == Color::Green) {
    // ...
}

The named values are the enumerators. Outside the enumeration you reach them with the scope operator, as in Color::Green.

Values. Underneath, an enumeration is an integer. The enumerators count up from zero unless you give explicit values, which you should do when the numbers have a meaning outside your program:

cpp
enum class Status { Ok = 200, NotFound = 404, ServerError = 500 };

The integer type behind it is called the underlying type. It is int by default, so a Color takes 4 bytes. You can choose a smaller one, as in enum class Color : std::uint8_t, with the header <cstdint>, which makes the type 1 byte and helps keep structs compact, as lesson 2 explained.

Scoped and unscoped. The older form, enum Color { Red, Green, Blue };, comes from C and has two problems. Its enumerators are placed in the surrounding scope, so two enumerations that both contain Red clash. And it converts to an int silently, so if (c == 1) compiles, and comparing a Color with a value from a different enumeration compiles as well. The enum class form keeps the enumerators inside the enumeration and refuses implicit conversions in both directions (step 5). Use enum class by default.

Explicit conversions. To get the integer, write static_cast<int>(c) (step 3). To go the other way, write static_cast<Color>(1) (step 4). The cast does not check that the number matches an enumerator. For an enumeration with a fixed underlying type, which every enum class has, any value of that type is allowed, even one with no name.

Using a switch. The enumeration and the switch statement from lesson 1 of module 2 fit together:

cpp
std::string name(Color c) {
    switch (c) {
        case Color::Red:   return "red";
        case Color::Green: return "green";
        case Color::Blue:  return "blue";
    }
    return "unknown";
}

There is deliberately no default. If you later add an enumerator and forget to handle it, the compiler warns, as long as -Wall is on. The final return covers the case of a number cast into the type that has no name.

Printing. An enum class has no << operator, so std::cout << c does not compile. A function like name above is the usual answer.

In structs. An enumeration member makes a struct self-describing: struct Task { std::string title; Status state; };.

Why it is correct

An enumeration is a distinct type, so the compiler can tell a Color from a Direction and from an int, and it rejects mixing them. Because the set of values is closed and known, switch can be checked for completeness. And because the value is stored as an integer, all of this costs nothing at run time: Color::Blue is replaced by the number 2 at compile time, as step 2 shows.

Scoping solves a problem of scale. In a large program many enumerations share the natural names such as None, Error and Red. Qualifying each enumerator by its type removes the clashes without inventing prefixes.

What it costs

Run time. Nothing. An enumerator is a constant in the machine code, and a switch over dense enumerators can compile to a jump table, as in lesson 1 of module 2.

Memory. The underlying type decides. An array of a million Color values takes 4 MB with the default int, and 1 MB with std::uint8_t. Inside a struct, a one-byte enumeration can sit next to other small members and avoid padding.

Maintenance. Adding an enumerator means updating each switch and each conversion function, but the warning points at the switches for you. That is a feature rather than a cost.

Pitfalls and variations

Plain enums. They leak names and convert silently. Use enum class.

A default label. Writing default: in a switch over an enumeration silences the missing-case warning. Leave it out, and handle the unknown value after the switch.

Casting any number. static_cast<Color>(17) is legal and yields a value that has no name. Validate a number that comes from outside the program before casting it.

Persisted values. If you store the numbers in a file, as the later lessons of this module do, the file depends on the order of the enumerators. Reordering them silently breaks old files. Give explicit values to anything that is stored.

Bit flags. Combining enumerators with | does not compile for an enum class unless you overload the operators, which the Core level shows. The standard library uses such types for the file-opening modes of the next lessons.

Variations. C++20's using enum Color; brings the enumerators into a scope, which shortens a switch. C++23's std::to_underlying replaces static_cast<int>. A std::variant, in the Core level, covers choices that carry data. Automatic conversion of an enumerator to its name is a long-wanted feature, and static reflection, planned for C++26, is meant to provide it.

Where it is used

State machines, error codes, command types and protocol opcodes are enumerations. The standard library uses them throughout: std::errc lists error conditions, std::byte is an enum class, and the opening mode of a file stream is a set of flags. That last use leads directly into the next lesson.

The next lesson, "Writing Text Files With ofstream", moves data out of the program and onto the disk.

04 · Prove it

Question 1 of 4 · answer from memory

What is `sizeof` of `enum class Color { Red, Green, Blue };` on a typical platform, and which integer is `Color::Blue`?

05 · Practice

Something wrong with this lesson? Report a problem

Not started

Sign in to save your learning progress.

Sign in to save