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 class definitions do not conflict and names must be accessed using the scope resolution operator (::)

  • They are not implicitly convertible to/from integers. Use static_cast to 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 enum or enum class has 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 false with 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 class istead of enum.

  • 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.

Additional resources