Tip of the Week #86: Enumerating with Class

Originally posted as totw/86 on 2015-01-05

By Bradley White ([email protected])

“Show class, … and display character.” - Bear Bryant.

An enumeration, or simply an enum, is a type that can hold one of a specified set of integers. Some values of this set can be given names, and are called the enumerators.

Unscoped Enumerations

This concept will be familiar to C++ programmers, but prior to C++11 enumerations had two significant shortcomings: the enumeration names were:

  • in the same scope as the enum type, and
  • implicitly convertible to values of some integer type.

So, with C++98 …

enum CursorDirection { kLeft, kRight, kUp, kDown };
CursorDirection d = kLeft; // OK: enumerator in scope
int i = kRight;            // OK: enumerator converts to int

but, …

// error: redeclarations of kLeft and kRight
enum PoliticalOrientation { kLeft, kCenter, kRight };

C++11 modified the behavior of unscoped enums in one way: the enumerators are now local to the enum, but continue to be exported into the enum’s scope for backwards compatibility.

So, with C++11 …

CursorDirection d = CursorDirection::kLeft;  // OK in C++11
int i = CursorDirection::kRight;             // OK: still converts to int

but the declaration of PoliticalOrientation would still elicit errors.

Scoped Enumerations

The implicit conversion to integer has been observed to be a common source of bugs, while the namespace pollution caused by having the enumerators in the same scope as the enum causes problems in large, multi-library projects. To address both these concerns, C++11 introduced a new concept: the scoped enum.

In a scoped enum, introduced by the keywords enum class, the enumerators are:

  • only local to the enum (they are not exported into the enum’s scope), and
  • not implicitly convertible to integer types.

So, (note the additional class keyword) …

enum class CursorDirection { kLeft, kRight, kUp, kDown };
CursorDirection d = kLeft;                    // error: kLeft not in this scope
CursorDirection d2 = CursorDirection::kLeft;  // OK
int i = CursorDirection::kRight;              // error: no conversion

and, …

// OK: kLeft and kRight are local to each scoped enum
enum class PoliticalOrientation { kLeft, kCenter, kRight };

These simple changes eliminate the problems with plain enumerations, so enum class should be preferred in all new code.

Using a scoped enum does mean that you’ll have to explicitly cast to an integer type should you still want such a conversion (e.g., when logging an enumeration value, or when using bitwise operations on flag-like enumerators). Hashing with std::hash will continue to work though (e.g., std::unordered_map<CursorDirection, int>).

Underlying Enumeration Types

C++11 also introduced the ability to specify the underlying type for both varieties of enumeration. Previously the underlying integer type of an enum was determined by examining the sign and magnitude of the enumerators, but now we can be explicit. For example, …

// Use "int" as the underlying type for CursorDirection
enum class CursorDirection : int { kLeft, kRight, kUp, kDown };

Because this enumerator range is small, and if we wished to avoid wasting space when storing CursorDirection values, we could specify char instead.

// Use "char" as the underlying type for CursorDirection
enum class CursorDirection : char { kLeft, kRight, kUp, kDown };

The compiler will issue an error if an enumerator value exceeds the range of the underlying type.

Conclusion

Prefer using enum class in new code. You’ll reduce namespace pollution, and you may avoid bugs in implicit conversions.

enum class Parting { kSoLong, kFarewell, kAufWiedersehen, kAdieu };

Subscribe to the Abseil Blog