Enumerations in C++
Paul J. Lucas
Posted on October 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 viausing
(ortypedef
) 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 namespace
like:
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.
Posted on October 22, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.