02 - enum
You already know that magic numbers are bad.
It's much better to use named constants - they increase code readability.
It's quite common to have a few closely related constants. To signal that they are related, we could add a common name prefix but this still doesn't prevent of mistakes like this:
There is a dedicated feature for a group of constants:
Enumerations are an example of user-defined types - types not defined by the language but created in code by the user of the language (here user means programmer). Specific values of given enumeration are called enumerators. Enumerations are implicitly convertible to integers.
Enumerations were already present in C. The feature is relatively simple, not much more than just a syntax sugar for specifying constants. There are 2 particular problems caused by this simplicity:
1 2 3 4 5 6 7 8 |
// 1) no scoping - name conflicts enum color { red, green, blue }; enum light { red, yellow, green }; // error: red and green already defined // 2) no type safety void set_traffic_light(light l); color c = green; set_traffic_light(c); // type mismatch but no error! |
Why the function call here is not a compiler error?
The enum is implicitly converted to an integer and then this integer is implicitly converted to a different enumeration type.
enum class
C++11 added scoped enumerations, sometimes called strongly typed enumerations. They are defined with enum class or enum struct (both equivalent but the former became the convention) and solve the just mentioned problems:
They behave as proper types: enumerator names from different
enum classdefinitions do not conflict and names must be accessed using the scope resolution operator (::)They are not implicitly convertible to/from integers. Use
static_castto do so. As a consequence, mismatched enumeration types (e.g. when calling a function) will cause compiler errors.
1 2 3 4 5 6 7 8 |
enum class color { red, green, blue }; enum class light { red, yellow, green }; // color::red and light::red do not conflict void set_traffic_light(light l); color c = green; // error: no "green" in current scope color c = color::green; // ok auto c = color::green; // ok, now even better set_traffic_light(c); // error: parameter 1 expects light but got color |
Enumerator values
unless explicitly specified, each enumerator has value of the previous one + 1
unless explicitly specified, the first enumerator has value 0
there can be multiple enumerators with the same value
there can be gaps within the value range
1 2 3 4 5 6 7 8 9 10 11 12 |
enum class my_enum { a, // 0 b, // 1 c, // 2 d = 5, // 5 e, // 6 f, // 7 g = e + 6, // 12 h, // 13 i // 14 }; |
Underlying type
Since enumerations are an abstraction over constant integers, it's possible to specify on which integral type they should be implemented:
1 2 3 4 5 6 |
// ok enum class e1 : char { a, b, c, d }; // error: value -1 outside range of unsigned char enum class e2 : unsigned char { a, b, c, x = -1 }; // error: float is not an integral type enum class e3 : float { a, b, c, d }; |
Since C++11 the standard library contains a type trait that can be used to obtain enumeration's underlying type. Since C++23 there is also a function that converts the enumeration to its udnerlying type. Below I present a C++11-compatible implementation of this function. With such function, you can easily and safely convert enumerations to their underlying types with minimal effort.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// since C++11 template <typename T> struct underlying_type { using type = /* compiler magic */; }; // since C++14 (short alias) template <typename T> using underlying_type_t = typename underlying_type<T>::type; // since C++23 available as std::to_underlying in <utility> // here a C++11-compatible implementation // with this function you can write to_underlying(some_enum_value) // which is much shorter than static_cast with a type trait template <typename Enum> constexpr typename underlying_type<Enum>::type to_underlying(Enum e) noexcept { return static_cast<typename underlying_type<Enum>::type>(e); } |
Convertions in detail
If the
enumorenum classhas specified underlying type, then all convertions to/from act as if the enumerator had value of this type.If the underlying type is not specified, the largest allowed value is the largest representable value in the smallest bitfield capable of holding enumeration values. No overflow, just undefined behavior.
It's possible to convert a value of integral type to an enumeration type where no enumerator has such value. Object of the enumeration type will simply compare
falsewith every enumerator.
No idea. In theory, it allows the compiler to use less bits than required by the type but no compiler actually does such optimization - first, you can't have sizeof which is not a multiple of whole bytes, second, saving space this way complicates read/write instructions and in today's world we have much higher pressure on computing time, not memory. Some optimizations even intentionally cause the code to occupy more space in order to speed up execution (e.g. -falign-functions).
Can I convert an enum to/from a string?
Sadly no. Compared to other languages, enums in C++ are pretty basic - they are just a different form of writing constants with some added type safety. If you want more features, use a library like Better Enums which offers a macro that defines enumerations and the boilerplate needed to match strings with enumerators.
Recommendation
Use
enum classistead ofenum.Use enumerations when you have a group of related constants and each represents one possibility (which exludes others).
Explicit values of enumerators should be only specified when there is a need to convert them to/from integers.
-
Enumerators with the same value generally should be avoided (it's kind of duplicate code) but they are useful in certain situations:
Backwards compatibility: old name can still function while being equivalent to the new name.
Future proofing: multiple enumerators can be introduced and if it's realized later that situations they denote are identical, values can be equivalent (differently named enumerators with the same assigned value compare equal).
Clarity: in cases when there is a finite amount of things that could happen and some require the same reaction. Listing them all is still valuable - otherwise someone reading the code could think that some were forgotten.