Enumerations in C++

Paul J. Lucas
4 min readOct 22, 2023

--

Introduction

C++ inherited enumerations from C, warts and all. Everything about enumerations in C also applies in enumerations C++ (so if you haven’t read that article, you should). The problems with enumerations in C are:

  • Constants are not scoped; instead, they are “injected” into the surrounding scope. Sometimes, this causes name collisions.
  • Values implicitly convert to their underlying integral value and vice versa. While sometimes convenient, this can lead to bugs silently creeping in.
  • Enumerations can not be forward-declared.

C++11 extended enumerations to fix all of these problems.

enum class

So as to remain backwards-compatible with C, enumerations weren’t touched. Instead, C++11 added enumeration classes that fix all of the issues with C-style enumerations:

enum class color {  // Note "class" keyword.
BLACK,
WHITE,
BLUE,
GREEN,
RED,
};

Incidentally, enum struct can also be used, but there’s no difference.

Constant Scoping

Enumeration classes immediately fix the scoping problem in that enumeration constants are not “injected” into the surrounding scope. Instead, they’re referred to via the :: operator:

color c = color::BLACK;

For C-style enumerations, :: can also be used even though it isn’t necessary.

Constant Conversion

Unlike C-style enumerations, enumeration class constants do not implicitly convert to their underlying integral values:

int n = color::RED;                      // error

Instead, an explicit cast is required:

int n = static_cast<int>( color::RED );  // OK

Bit Flag Values

If enumeration class constants do not implicitly convert to their underlying integral values, then the bitwise operators don’t work:

enum class c_int_fmt {
NONE = 0,
SHORT = 1 << 0,
INT = 1 << 1,
LONG = 1 << 2,
UNSIGNED = 1 << 3,
CONST = 1 << 4,
STATIC = 1 << 5,
};

c_int_fmt f = c_int_fmt::UNSIGNED | c_int_fmt::INT; // error

However, you can overload the bitwise operators:

constexpr c_int_fmt operator|( c_int_fmt lhs, c_int_fmt rhs ) {
using U = std::underlying_type_t<c_int_fmt>;
return static_cast<c_int_fmt>( static_cast<U>(lhs) | static_cast<U>(rhs) );
}

You can overload the other bitwise operators &, ^, ~, |=, &=, and ^= similarly. However, if you agree that it’s rather tedious to overload seven operators for every enumeration class you’re using for bit flag values, you define a macro like:

#define ENABLE_BITWISE_OPERATORS_FOR_ENUM(E)                            \
static_assert( std::is_enum_v<E>, "enumeration type required" ); \
constexpr E operator|( E lhs, E rhs ) { \
using U = std::underlying_type_t<E>; \
return static_cast<E>( static_cast<U>(lhs) | static_cast<U>(rhs) ); \
} \
constexpr E operator&( E lhs, E rhs ) { \
using U = std::underlying_type_t<E>; \
return static_cast<E>( static_cast<U>(lhs) & static_cast<U>(rhs) ); \
} \
constexpr E operator^( E lhs, E rhs ) { \
using U = std::underlying_type_t<E>; \
return static_cast<E>( static_cast<U>(lhs) ^ static_cast<U>(rhs) ); \
} \
constexpr E operator~( E e ) { \
using U = std::underlying_type_t<E>; \
return static_cast<E>( ~static_cast<U>( e ) ); \
} \
constexpr E operator|=( E &lhs, E rhs ) { \
return (lhs = lhs | rhs); \
} \
constexpr E operator&=( E &lhs, E rhs ) { \
return (lhs = lhs & rhs); \
} \
constexpr E operator^=( E &lhs, E rhs ) { \
return (lhs = lhs ^ rhs); \
} \
using type_to_eat_semicolon = int

And then use it like:

ENABLE_BITWISE_OPERATORS_FOR_ENUM( c_int_fmt );

The type_to_eat_semicolon is a common trick used when you want to use ; after a use of a macro, but the macro’s definition ends with something that doesn’t allow a ; to follow it, in this case the closing } of a function definition. The trick works because it’s legal to alias a type via using (or typedef) multiple times so long as the aliased type is the same.

Forward Declaration

Enumerations can be forward-declared, but only when the underlying type is specified:

enum class color : uint8_t;

Forward declaration is also allowed for C-style enumerations so long as the underlying type is specified.

Older C++ Code

In pre-C++11 code you may encounter, there was a trick to making C-style enumerations not inject their constants into the global scope:

namespace color {  // Pre-C++11 trick.
enum type {
BLACK,
WHITE,
BLUE,
GREEN,
RED,
};
}

That is, wrap the enumeration inside a namespace with the name you would have given the enumeration and always name the enumeration type:

color::type c = color::RED;

This trick is no longer necessary. However, it’s still perfectly fine and useful to be able to put enumerations inside either classes or namespaces for the same reasons you’d put anything inside classes or namespaces.

Nested Enumerations

If you have an enumeration class nested inside either a class or namespacelike:

namespace vt100 {
enum class color {
BLACK,
WHITE,
BLUE,
GREEN,
RED,
};
// ...
}

Then when using it:

vt100::color c = vt100::color::RED;  // Verbose.

It gets a bit verbose to have to always specify color when though you’ve already specified vt100. Starting in C++20, you can “import” enumeration constant names into their enclosing scope:

namespace vt100 {
enum class color {
BLACK,
WHITE,
BLUE,
GREEN,
RED,
};
using enum color; // Import constants into vt100 namespace.
// ...
}

Now you can instead do:

vt100::color c = vt100::RED;         // Better.

Conclusion

All of the best practices that apply to enumerations in C also apply to enumerations in C++.

Additionally, in new C++ code, enumeration classes should be used exclusively, especially for those declared in the global scope so as not to “pollute” the global namespace with all their constant names. Use C-style enumerations only when compatibility with C is required.

--

--

Paul J. Lucas

C++ Jedi Master. I am NOT available for advice, consultation, recommendations, nor individual training. No, I don't want to write for your publication or site.