8.8. Enumerated types

An enum (enumerated type) is a distinct type whose value is restricted to a range of values (see below for details), which may include several explicitly named constants ("enumerators"). The values of the constants are values of an integral type known as the underlying type of the enumeration.

There are two distinct kinds of enumerations:

unscoped enumeration

declared with the keyword enum

scoped enumeration

declared with the enum-key enum class or enum struct

8.8.1. Unscoped enumerations

Each enumerator becomes a named constant of the enumeration's type (that is, name), visible in the enclosing scope, and can be used whenever constants are required.

enum color { red, green, blue };
color r = red;
switch(r)
{
  case red  : std::cout << "red\n";   break;
  case green: std::cout << "green\n"; break;
  case blue : std::cout << "blue\n";  break;
}

Each enumerator is associated with a value of the underlying type. When initializers are provided in the enumerator-list, the values of enumerators are defined by those initializers. If the first enumerator does not have an initializer, the associated value is zero. For any other enumerator whose definition does not have an initializer, the associated value is the value of the previous enumerator plus one.

Values of unscoped enumeration type are implicitly-convertible to integral types. If the underlying type is not fixed, the value is convertible to the first type from the following list able to hold their entire value range: int, unsigned int, long, unsigned long, long long, or unsigned long long. If the underlying type is fixed, the values can be converted to their promoted underlying type.

enum color { red, yellow, green = 20, blue };
color c = red; // c  implicitly converts to 0
int n = blue;  // n == 21

Consider the following program:

Problems caused by unscoped enumerations
 1#include <iostream>
 2
 3enum direction { north, south, east, west };
 4
 5void show_direction(int direction) {
 6  std::cout << "Direction: " << direction << '\n';
 7}
 8
 9int main() {
10  direction dir = west;
11  show_direction(dir);
12  int num = dir;
13  show_direction(num);
14
15  for (int i = north; i < 8; ++i) {
16    show_direction(i);
17  }
18}
What problems does this program have?
  1. The unscoped enum direction is not type safe

    • Any integral type can be assigned to it

  2. The line int num = dir; assigns a direction to an int

  3. The for loop is unaware that there are only 4 directions, not 8.

  4. Function show_direction implies it takes a direction as an argument, but any integral type will be processed without complaint

  5. The function show_direction can't print a human readable direction. It can only print a number.

Scoped enums were introduced to address these specific shortcomings.

8.8.2. Scoped enumerations

Like unscoped enumerations, each enumerator becomes a named constant of the enumeration's type visible in the enclosing scope, and can be used whenever constants are required.

Unlike unscoped enums, scoped enums do not implicitly convert to integral types, but can be converted with a static or explicit cast.

enum class color { red, green = 20, blue };
color r = color::blue;
switch(r)
{
  case color::red  : std::cout << "red\n";   break;
  case color::green: std::cout << "green\n"; break;
  case color::blue : std::cout << "blue\n";  break;
}
// int n = r;                // error: no implicit int conversion
int n = static_cast<int>(r); // OK, n = 21

C++23 Feature

Starting in C++23, std::to_underlying provides a named standard-library function for the same conversion. These statements are equivalent:

std::to_underlying(color::red)

static_cast<int>(color::red)

All the C++ enumerated types are very simple compared to the same types in other languages. However, it is easy to add features as needed. Given the following scoped enum:

// If this were declared enum struct, nothing in this example changes...
//
enum class direction { north, south, east, west };

We can implement a stream overload to allow printing enum members with human readable strings:

std::ostream& operator<<(std::ostream& os, direction dir) {
  switch (dir) {
    case direction::north: return os << "north";
    case direction::east:  return os << "east";
    case direction::south: return os << "south";
    case direction::west:  return os << "west";
  }
  return os;
}

And an array allows the enum to be used in a range for loop:

constexpr std::array directions {
  direction::north,
  direction::east,
  direction::south,
  direction::west
};

Now the show directions program looks like this:

Printing and iterating scoped enum values
 1#include <array>
 2#include <iostream>
 3
 4enum class direction { north, south, east, west };
 5
 6std::ostream& operator<<(std::ostream& os, direction dir) {
 7  switch (dir) {
 8    case direction::north: return os << "north";
 9    case direction::east:  return os << "east";
10    case direction::south: return os << "south";
11    case direction::west:  return os << "west";
12  }
13  return os;
14}
15
16constexpr std::array directions {
17  direction::north,
18  direction::east,
19  direction::south,
20  direction::west
21};
22
23void show_direction(direction dir) {
24  std::cout << "Direction: " << dir << '\n';
25}
26
27int main() {
28  direction dir = direction::west;
29  std::cout << "Show one direction:\n";
30  show_direction(dir);
31
32  std::cout << "Loop through all directions:\n";
33  for (const auto dir: directions) {
34    show_direction(dir);
35  }
36}

Another example uses the same techniques to build a deck of cards.

 1#include <array>
 2#include <iostream>
 3#include <vector>
 4
 5enum class rank {
 6  ace = 1, two, three, four, five, six, seven, eight, nine, ten, jack, queen, king
 7};
 8
 9// Define an array of rank to allow range-for looping
10constexpr std::array ranks {
11  rank::ace, rank::two, rank::three, rank::four, rank::five,
12  rank::six, rank::seven, rank::eight, rank::nine, rank::ten,
13  rank::jack, rank::queen, rank::king
14};
15
16enum class suit {
17  clubs, diamonds, hearts, spades
18};
19
20// Define an array of suit to allow range-for looping
21constexpr std::array suits {
22  suit::clubs, suit::diamonds, suit::hearts, suit::spades
23};
24
25struct card {
26  rank card_rank;
27  suit card_suit;
28
29  card(rank r, suit s)
30    : card_rank{r},
31      card_suit{s}
32  {
33  }
34};
35
36std::ostream& operator<<(std::ostream& os, rank value);
37std::ostream& operator<<(std::ostream& os, suit value);
38std::ostream& operator<<(std::ostream& os, const card& value);
39
40// short, but take care . . .
41// a change in the order of the enum could break this,
42// Assumes order: A,2,3,4,5,6,7,8,9,10,J,Q,K
43std::ostream& operator<<(std::ostream& os, rank value) {
44  if (value > rank::ace && value < rank::jack) {
45    os << static_cast<int>(value);
46  } else if (value == rank::ace) {
47    os << "Ace";
48  } else if (value == rank::jack) {
49    os << "Jack";
50  } else if (value == rank::queen) {
51    os << "Queen";
52  } else {
53    os << "King";
54  }
55
56  return os;
57}
58
59std::ostream& operator<<(std::ostream& os, suit value) {
60  switch(value) {
61    case suit::clubs:    os << "Clubs"; break;
62    case suit::diamonds: os << "Diamonds"; break;
63    case suit::hearts:   os << "Hearts"; break;
64    case suit::spades:   os << "Spades"; break;
65  }
66  return os;
67}
68
69std::ostream& operator<<(std::ostream& os, const card& value) {
70  return os << value.card_rank << " of " << value.card_suit;
71}
72
73int main() {
74  std::vector<card> deck;
75  for (const auto s: suits) {
76    for (const auto r: ranks) {
77      deck.emplace_back(r, s);
78    }
79  }
80  for (const auto& c: deck) {
81    std::cout << c << '\n';
82  }
83}

More to Explore