Enumerations in Modern C++

Enumerations have existed in C++ since the beginning, having come from C. However, they had some issues and the solution was a new enumeration type in C++11. This article details both enumeration types and provides the rationale behind the new enumeration type.

Enumerations

An enumeration is a distinct type with a set of named integral constants as its domain. For example, a day of the week enumeration can be defined as follows:

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

The enumeration type is day, while mon through sun are its enumerators. The enumerators are in the same scope as the enumeration, the immediately surrounding scope. Enumerators can be initialised with explicit values:

enum foo { a, b = 2, c }; // 0, 2, 3

Any enumerator not initialised is given the value of the previous enumerator plus one (C++03 §7.2.1). In the above case, this means c has the value three. If the first enumerator is not explicitly initialised then its value is zero, like a above (C++03 §7.2.1). Enumerator values can be duplicated, allowing aliasing:

enum foo { a, b, c, alpha = 0 }; // 0, 1, 2, 0

An initialiser must be constant expressions of integral or enumeration type (C++03 §7.2.1):

int x = 42;
enum foo {
    a = false, // 0
    b = true, // 1
    c = x, // error, 'x' is not usable in a constant expression
    d = 3.5 // error, 'd' is not an integer constant
};

An enumerator takes the type of the enumeration that defined it (C++03 §7.2.4). You can refer to previously defined enumerators:

enum foo { a, b, c, alpha = a }; // 0, 1, 2, 0

You can refer to enumerators from previously defined enumerations due to integral promotion, discussed later:

enum latin { a, b }; // 0, 1
enum greek { alpha = a, beta = b }; // 0, 1

An enumerator is not defined until after its definition, so it cannot refer to itself (C++03 §7.2.3):

const int x = 42;
{ enum foo { x = x }; } // x is 42

The enumerator x is initialised by the constant x and has the value 42. The extra braces around the enumeration definition are required to avoid a name clash between the constant and enumerator.

Enumerations forbid assignments between two different enumerations, as well as there being no implicit integer to enumeration conversion:

enum latin { a, b };
enum greek { alpha, beta };

greek foo = a; // error, cannot convert latin to greek
greek bar = 1; // error, cannot convert int to greek

However, enumerations do allow implicit conversion to integer (C++03 §7.2.8) through integral promotion:

int foo = beta; // 1
bool bar = beta > 0; // true

Enumerations can be explicitly converted from integers. If the value is outside the enumerators range, the enumeration value is unspecified (C++03 §7.2.9):

greek x = static_cast<greek>(1);

The underlying type of an enumeration is an implementation specified integral type that can represent all the enumerator values. It will not be larger than an int unless an enumerator's value exceeds an int or unsigned int (C++03 §7.2.5). The underlying type can be discovered using underlying_type in C++11:

#include <type_traits>

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

using type = std::underlying_type<day>::type;
using type2 = std::underlying_type_t<day>; // C++14 equivalent

It is legal to have an empty enumerator list, which sounds pointless but is particularly useful for safer integers. It is treated, for purposes such as calculating the underlying type, as if it had a single enumerator initialised to zero:

enum foo {}; // treated as if it were enum foo { x = 0 };

Issues with C++03 enumerations

Enumerations in C++03, while useful, have some issues that make them less than ideal.

Scoping

As enumerators are placed in the same scope as the enumeration type, it can cause name clashes:

enum traffic_light { red, yellow, green };
enum led_light { red, orange, yellow, blue, green };

Here red, yellow and green are all redeclared by led_light, resulting in compiler errors. A common workaround is to prefix all enumerators, usually with the enumeration name or some variation of it:

enum traffic_light { traffic_light_red, /* ... */ };

Implicit conversion to an integer

Implicit conversion to integer leaves a hole in the type system. It is possible to accidentally compare, for example, two different enumerations:

enum day { mon, tue, wed, thu, fri, sat, sun };
enum month { jan, feb, /* ... */ };

if (tue == feb) { // No error. Almost definitely a mistake

GCC and clang will warn about these comparisons, although they are permitted by the standard. It's part of the -Wenum-compare flag that is on by default. If you need to suppress this warning globally, you can use -Wno-enum-compare. However, it is probably best to add an operator overload or use explicit casts for specific exceptions while leaving the warning on:

if (static_cast<int>(tue) == static_cast<int>(feb)) {

Inability to specify an underlying type

As the user cannot make any assumptions about the underlying type, a user also cannot even attempt to size structures reliably. Reading plain old data types (PODs) directly to and from memory or disk is very common in C and C++. However, this depends on an exact structure layout, which is already somewhat complicated to do portably.

Imagine a protocol or file type you need to parse. Let's say that it starts the header block with a version number stored in one byte and there are three versions. This seems like a good application for an enumeration:

enum version { version_1, version_1_mini, version_2 };

struct header {
    version ver; // this could be sizeof(int)
    int length;
    /* ... */
};

Although the semantics are nicely representated by the enumeration, we cannot be sure what type underlies version. Consequently, we cannot use the enumeration here and must explicitly use a type with the correct size, such as uint8_t. Even if your compiler did use a type that worked in this circumstance, it would not be portable. Another compiler, or a future version of the same compiler, may decide the underlying type differently and break your program.

Furthermore, you cannot make assumptions about the signedness of the underlying type. This can be somewhat counterintuitive in cases that seem to suggest otherwise:

enum foo {
    a,
    b = 0xffffffffu // unsigned integer initialiser
};

You might expect that unsigned literal would cause the compiler to select an unsigned integer as the underlying type. However, a compiler is under no such obligation and some choose a signed integer in this circumstance. This leads to a < b being true for some compilers (b is -1 when converted to a signed integer under two's complement) and false for others. More details can be seen in the proposals linked later.

Prototyping

The inability to specify underlying types also means enumerations cannot be prototyped. All enumerators must be known for the compiler to decide on the underlying type. Imagine the following simplified scenario:

// header file
enum foo; // assume prototyping like this was legal

struct bar {
    foo x;
    int y;
};

// source file
enum foo { a, b };

The compiler could not know, from the header file alone, the size of foo. Therefore the size of any dependencies, such as the structure bar, would also be unknown.

Enumerations cannot be prototyped so all the enumerators must be in the header file. Adding or removing an enumerator can cause a lot of recompilation, especially in a common header file, even though the underlying type of the enumeration may not change.

Strongly typed enumerations in C++11

To address these issues, five papers were presented from 2003 through 2007:

These proposed an extended form of strongly typed enumeration, called scoped enumerations in the C++11 standard, to solve both the scoping and the implicit conversion issues. The original enumerations of C++03, now called unscoped enumerations, exist with unchanged semantics to maintain backwards compatibility. It was also proposed to optionally specify an underlying type for both scoped and unscoped enumerations. These proposals were accepted into C++11.

The syntax for a scoped enumeration is the same as an unscoped enumeration but with the class or struct keyword after enum:

enum unscoped { foo };
enum class scoped { bar };
enum struct scoped_too { baz };

Scoping

Scoped enumerations, in accordance with their name, introduce a new scope for their enumerators:

enum unscoped { foo };
enum class scoped { bar };

unscoped x = foo; // okay, foo is in scope
scoped y = bar; // error, bar is not in scope

You can reference the enumerators through the scope resolution operator:

scoped y = scoped::bar;

To make it easier to write generic code to handle either type of enumeration, you can use the same scoping syntax for unscoped enumerations, although it is redundant:

unscoped x = foo;
unscoped y = unscoped::foo; // equivalent

This solves the original example of name clashing:

enum class traffic_light { red, yellow, green };
enum class led_light { red, orange, yellow, blue, green, white };

traffic_light x = traffic_light::red;
led_light y = led_light::red;

Implicit conversions

Scoped enumerations do not implicitly convert to integer or bool:

enum class day { mon, tue, wed, thu, fri, sat, sun };
enum class month { jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec };

int a = day::wed; // error, cannot convert day::wed to int 
bool b = month::feb > 0; // error, no match for operator>

Comparisons between different scoped enumerations are also caught as errors, not warnings:

if (day::tue == month::feb) { // error, no match for operator==

Underlying type

The underlying type of both scoped and unscoped enumerations can be specified (called fixed by the standard, C++11 §7.2.5):

enum class greek : unsigned char { alpha, beta };

Any integral type is valid, meaning bool, char, char16_t, char32_t, wchar_t, short, int, long and long long along with unsigned and cv-qualified variants. If bool is the underlying type, valid values are only zero and one. wchar_t was excluded in the last proposal (n2347) but is included in the standard. Allowing floating-point types was originally proposed (n1513) but dropped in the next proposal. The program is ill-formed if the enumerator value cannot be represented by the underlying type.

The default underlying type of a scoped enumeration is an int:

enum class greek { alpha, beta };
static_assert(std::is_same<std::underlying_type<greek>::type, int>::value, "");

The default underlying type of an unscoped enumeration remains implementation specified for backwards compatibility.

Prototyping

If the underlying type is specified the issue preventing prototyping is also removed. As such, you can prototype any enumeration so long as its underlying type is specified:

enum foo : short;
enum class bar : short;
enum class baz; // default underlying type is int

struct baz {
    foo x; // sizeof(short)
    bar y; // sizeof(short)
    baz z; // sizeof(int)
};

Hybrid enumerations

Scoped enumerations provide both scoping and stronger typing. In most cases this is desirable. However, it may be the case that you want only one of these two qualities. C++ allows us to emulate this.

Scoped weakly typed enumerations

It is possible to emulate scoping using a namespace, class or struct wrapped around an unscoped enumeration:

struct day {
    enum type { mon, tue, wed, thu, fri, sat, sun };
};

day::type tomorrow = day::mon;
int i = day::mon;

The code above is very similar code for scoped enumerations. The name day must prefix the enumerators as they are not in scope. However, a small difference is that you must say day::type and not just day, although type inference can help here. The last line shows that implicit integer conversion still works.

Unscoped strongly typed enumerations

Scoped enumerations can emulate being unscoped by bringing the enumerators into the immediately enclosing scope:

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

constexpr auto mon = day::mon;
constexpr auto tue = day::tue;
constexpr auto wed = day::wed;
constexpr auto thu = day::thu;
constexpr auto fri = day::fri;
constexpr auto sat = day::sat;
constexpr auto sun = day::sun;

day tomorrow = mon;
int i = mon; // error, as desired

The process works well but requires manually adding a declaration per enumerator. This approach is easier if the code is generated by a program or static reflection.

Miscellaneous

Trailing commas

Since C++11, trailing commas are permitted in enumerations (C++11 §7.2.1). The following code is therefore valid C++11 but invalid C++03:

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

Anonymous enumerations

If the name of an unscoped enumeration is omitted the result is known as an anonymous enumeration:

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

The enumerators exist in the immediate enclosing scope and can be used as usual. This is not possible for scoped enumerations. Scoped enumerators exist only inside the enumeration's scope and this scope is inaccessable without a name for scope resolution.

enum class { foo, bar, baz }; // error, anonymous scoped enum is not allowed

It is possible to use anonymous enumerations to define constants for compile-time use.

Safer integers

As enumerations are distinct types and scoped enumerations are strictly typed, they can be used to introduce distinct integer types within a program. It is possible to catch certain errors at compile-time:

enum class metres {}; // underlying type is int
enum class feet {};

void important_calculation(feet);

auto distance = static_cast<metres>(10);

important_calculation(distance); // error, distinct types
important_calculation(static_cast<feet>(10)); // okay, verbose

If standard integer types had been used here, this error would not be a compile- or run-time error. A notable example is the Mars Climate Orbiter, where two different units of measurement silently clashed and caused the loss of a space probe worth over $100 million.

The primary downside is the inconvenience, noise and verbosity caused by the casts. C++17 has improved this situation, see below.

Construction in C++17

Enumerations are particularly useful as distinct integer types but the inconvenience of construction is an impediment to use. Gabriel Dos Reis submitted the "Construction Rules for enum class Values" proposal to address this. It was accepted and now C++17 allows an implicit, non-narrowing conversion from the underlying type of an enumeration:

enum class metres {}; // underlying type is int

metres foo{42};
metres bar = {42};
metres baz = metres{42};
auto qux = metres{42};

The enumeration must declare no enumerators for this to work. To maintain backwards compatibility, it is not possible to elide the type name for a function call:

enum class metres {};

void calculation(metres);

calculation({42}); // error
calculation(metres{42}); // okay

These new construction rules also apply to unscoped enumerations but only when they have an underlying type specified.

Operator overloading

Enumerations are distinct types so operator overloading is possible (C++03 §13.1.3; D&E §11.7.1):

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

day operator++(day d)
{
    return static_cast<day>((d + 1) % 7);
}

std::ostream& operator<<(std::ostream& out, day d)
{
    switch (d) {
        case mon: return out << "Monday";
        case tue: return out << "Tuesday";
        case wed: return out << "Wednesday";
        case thu: return out << "Thursday";
        case fri: return out << "Friday";
        case sat: return out << "Saturday";
        case sun: return out << "Sunday";
    }
}

Operator overloads can also be useful for scoped enumerations if you do need, for example, to compare different enumerations or test against numbers:

bool operator>(day d, int x)
{
    return static_cast<int>(d) > x;
}

bool operator==(day d, month m)
{
    return static_cast<int>(d) == static_cast<int>(m);
}

Type traits

C++11 introduced a new header, type_traits, designed to provide type information at compile-time. The is_enum trait can be used to identify enumerations at compile-time:

class a {};
union b {};
enum c {};
enum class d : short {};

std::is_enum<a>::value; // false
std::is_enum<b>::value; // false
std::is_enum<c>::value; // true
std::is_enum<d>::value; // true

It should be noted that although the underlying type of enumerations is always integral, is_integral returns false for enumerations:

std::is_integral<c>::value; // false
std::is_integral<d>::value; // false
std::is_integral<std::underlying_type<any enum>::type>::value; // true

Since C++17 you can use is_enum_v<x> in place of is_enum<x>::value. This pattern using the _v suffix is applied throughout the standard library.

Summary

Enumerations in C++03 are usable but suffer from weaker typing and practical limitations related to the underlying type.

C++11 introduced scoped enumerations to provide stronger typing, helping to reduce programmer error. Control over the underlying type for enumerations makes it practical to use them where it was difficult or impossible previously. Enumerations can now be prototyped, providing greater convenience and reducing compilation times in certain cases.

C++17 has made it much more convenient to use enumerations as distinct integer types, allowing stronger typing to catch more errors at compile-time.

References