Enumerations in C++

Dated Jun 4, 2022; last modified on Sat, 04 Jun 2022

Unscoped (or Plain or C-Style) Enumerations

Plain (or C-style) enums are entered in the same scope as the name of their enum, and implicitly convert to their integer value, e.g.

enum Color { red, green, blue };
int col = green;
Color c2 = 1; // error: invalid conversion from 'int' to 'Color' [-fpermissive]

enum CardColor { red, black }; // Error: "red" conflicts with a previous declaration

The name can be omitted if it’s not going to be used, e.g.

enum { red, green, blue };
int col = red;

.

Unnamed enums are common in code written before alternative ways of specifying integer constants, e.g.

enum { red = 0xFF0000, scale = 4, is_signed = 1 };

But we can now have constexpr int red = 0xFF0000;, etc.

The enumerator values can be specified. If omitted, the default values are zero for the first enum, and then

enum Foo { a, b, c = 0, d, e = 10, f = b + 30};
// a = 0, b = 1, c = 0, d = 1, e = 10, f = 31

Scoped Enumerations

enum class Color { red, blue, green };

enum class TrafficLight { red, green, yellow };

// The keywords `class` and `struct` are exactly equivalent.
// <span class="citation-ref"><a href="#cppReferenceEnumerationDeclaration"></a></span>
enum struct CardColor { red, black };

Color c1 = Color::red;
Color c2 = red; // error: 'red' was not declared in this scope
Color c3 = 2; // error: cannot convert 'int' to 'Color' in initialization

The scoping prevents collisions between enumerator names, e.g. Color::red is different from and can co-exist with TrafficLight::red.

C++20 added using-enum-declaration:

enum class Fruit { orange, apple };
enum class Color { red, orange };

void f() {
    using enum Fruit;
    Fruit f = orange;

    using enum Color; // error: 'Color Color::orange' conflicts with a previous declaration
}

Underlying Type

By default enum Color { red, blue } is backed by an implementation-defined integral type that can represent all enumerator values. If no integral type can represent all the enumerator values, the enumeration is ill-formed. A different underlying type can be specified.

enum Color : unsigned char { red, blue };
enum Foo : bool { bar, baz, qux }; // error: enumerator value '2' is outside the range of underlying type 'bool'
enum Bar : float {}; // error: underlying type 'float' of 'Bar' must be an integral type

// To forward-declare an enum, we need to specify the underlying type.
// <span class="citation-ref"><a href="#cppCoreGuidelinesEnums"></a></span>
enum Baz : char;
void f(Baz);

enum Baz : char {};

Built-in integral types are bool, char, char8_t, char16_t, char32_t, wchar_t, short, int, long and long long.

For space-efficiency, enum Foo : char {}; is the most compact, as the C++ Standard guarantees that 1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long). Note that sizeof(bool) is implementation-defined, and may differ from 1.

I’ve always considered ints as fast-enough. Would chars be significantly faster? 64-bit machines are the norm, hence the typical word size is 64 bits (or 8 bytes). int is typically 4 bytes and can thus fit in a word. Are there perf benefits for going below the word size?

Unscoped enums can implicit convert to their underlying type, but need a static cast when being initialized from the underlying type, e.g.

enum Color { red, green, blue };
int c1 = green; // Often undesirable as bugs may creep in.
Color c2 = static_cast<Color>(999); // OK, even if 999 is not a named enumerator
Color c3{2} // invalid conversion from 'int' to 'Color' [-fpermissive]

One approach for validating that enumerators are known is defining a function like bool IsKnownEnumValue(MyEnum value), e.g. , and then using it where the conversion from the integral type to an enumerator is performed.

Chromium defines checked_cast<>() for numeric types, which is like static_cast<>, except that it triggers a crash at runtime, or a compiler error if the conversion error can be detected at compile time.

From C++17, an enum may be initialized from an integer, without a cast, via list initialization, e.g.

enum Foo : unsigned char {}; // Foo can hold enumerators w/ values in [0, 255]

Foo f1{255}; // OK

Foo f2{-1}; // error: narrowing conversion of '-1' from 'int' to 'unsigned char' [-Wnarrowing]
Foo f3{256}; // error: narrowing conversion of '256' from 'int' to 'unsigned char' [-Wnarrowing]

int bar() { return 255; }
Foo f4{bar()}; // warning: narrowing conversion of 'bar()' from 'int' to 'unsigned char' [-Wnarrowing]

To get the underlying value, one can use the underlying_type utils:

enum Foo : char {};

Foo f{'b'};

std::underlying_type_t<Foo> v1 = static_cast<std::underlying_type_t<Foo>>(f);
std::underlying_type_t<Foo> v2 = std::to_underlying(f); // Shorthand; C++23

static_assert(std::is_same_v<char, decltype(std::to_underlying(f))>);

Given that a valid enumerator need not be named, use exhaustive switch statements responsibly:

enum Foo : unsigned char { bar, baz }; // Range [0, 255]

std::string ToStringBuggy(Foo foo) {
  switch (foo) {
      case Foo::bar: return "bar";
      case Foo::baz: return "baz";
  }
} // warning: control reaches end of non-void function [-Wreturn-type]

std::string ToString(Foo foo) {
  switch (foo) {
      case Foo::bar: return "bar";
      case Foo::baz: return "baz";
  }

  std::cerr << "Unknown Foo found.";
  return "kUnknownFoo";
}

int main() {
  std::cout << ToString(Foo{150}) << "\n"; // prints "kUnknownFoo"
  std::cout << ToStringBuggy(Foo{150}) << "\n"; // prints garbage, see https://godbolt.org/z/6Yq44MPc7
}

goes further than advocating for adding explicit return statements (and appropriate error logs) for fall-through cases. The objective of exhaustive switch statements is to ensure (via the -Wswitch compiler flag) that all enumerators are explicitly handled. This may not always be desirable, e.g. the owners of the enum are different from the owners of the exhaustive switch statements, but both code is in the same repository that the enum authors are blocked.

Operators for Enumerations

By default, an enum class only has assignment, initialization, and comparisons. However, it is a user-defined type, and so we can define operators for it, e.g.

enum class TrafficLight { red, green, yellow };

TrafficLight& operator++(TrafficLight& t) {
  switch (t) {
    case TrafficLight::green:   return t = TrafficLight::yellow;
    case TrafficLight::yellow:  return t = TrafficLight::red;
    case TrafficLight::red:     return t = TrafficLight::green;
  }
}

// Sample usage
TrafficLight light = TrafficLight::red;
TrafficLight next = ++light; // next becomes `TrafficLight::green`.

For longer enums with many cases, but the enumerations take consecutive values without holes, a static_cast helps with brevity:

enum class Day { mon, tue, wed, thu, fri, sat, sun };

Day& operator++(Day& d) {
  return d = (d == Day::sun) ? Day::mon : Day{static_cast<int>(d)+1};
}

Beware of infinite recursions in the name of avoiding a static_cast, e.g.

enum class Day { mon, tue, wed, thu, fri, sat, sun };

Day& operator++(Day& d) {
  return d = (d == Day::sun) ? Day::mon : Day{++d}; // runtime error
}

Style

Avoid ALL_CAPS for enumerators, as that may conflict with macros, e.g.

// web_colors.h (third party header)
#define RED   0xFF0000
// ... more definitions ...

// product_info.h
enum class Product_info { RED, PURPLE, BLUE };   // syntax error

Special names can help validate the code via compiler plugins. For example, Chromium has a clang plugin that checks that kMaxValue is indeed the max value, e.g.

enum class Foo {
  kOne, kTwo, kMaxValue = kOne,
};  // kMaxValue enumerator does not match max value 0 of other enumerators

How do I write a clang plugin? See Clang Plugins — Clang 15.0.0git documentation .

References

  1. A Tour of C++ (Second Edition). Chapter 2. User-Defined Types. Bjarne Stroustrup. 2018. ISBN: 978-0-13-499783-4 .
  2. Enumeration declaration - cppreference.com. en.cppreference.com . Accessed Jun 5, 2022.
  3. std::to_underlying - cppreference.com. en.cppreference.com . en.cppreference.com . Accessed Jun 5, 2022.
  4. std::is_integral - cppreference.com. en.cppreference.com . Accessed Jun 5, 2022.
  5. Fundamental types - cppreference.com. en.cppreference.com . Accessed Jun 5, 2022.
  6. sizeof operator - cppreference.com. en.cppreference.com . Accessed Jun 5, 2022.
  7. base/numerics/README.md > Conversion functions and StrictNumeric<> in safe_conversions.h. chromium.googlesource.com . Accessed Jun 5, 2022.
  8. Mojo docs (go/mojo-docs) - Mojo C++ Bindings API. chromium.googlesource.com . Accessed Jun 5, 2022.
  9. C++ Core Guidelines > Enum: Enumerations. isocpp.github.io . Accessed Jun 5, 2022.
  10. abseil / Tip of the Week #147: Use Exhaustive switch Statements Responsibly. abseil.io . Accessed Jun 5, 2022.
  11. Chromium Style Checker Errors > Enumerator Max Values. www.chromium.org . Accessed Jun 5, 2022.