Constants are great. Types are great. Constants of a specific type are really great. This is why enum classes are just fantastic.
Last year, we talked about why we should avoid using boolean function parameters. One of the solutions proposed uses strong types, in particular using enums instead of raw booleans. This time, let's see how enums and the related support evolved during the course of the years.
Unscoped enumerations
Enumerations are part of the original C++ language, in fact, they were taken over from C. Enumerations are distinct types with a restricted range of values. The range of values is restricted to some explicitly named constants. Let's quickly have a look at an enum.
enum Color { red, green, blue };
After having read this very small example, it's worth noticing two things:
- The
enumitself is a singular noun, even though it enumerates multiple values. We use such conventions because we keep in mind that it will be always used with one value. If you take aColorfunction parameter, one colour will be taken. When you compare against a value, you'll compare against one value. E.g. it reads better to compare againstColor::redthan againstColors::red - The enumerator values are not written in ALL_CAPS! Even though there is a fair chance that you are used to that. I also used to do that. So why didn't I follow that practice? Because for writing this article, I checked the core guidelines and Enum.5 clearly says that we should not use ALL_CAPS in order to avoid clashes with macros. By the way, Enum.1 clearly said that we should use enumerations over macros.
Since C++11, the number of possibilities to declare an enum grew. C++11 introduced the possibility of specifying the underlying type of an enum. If it's left undefined, the underlying type is implementation-defined but in any case, it's an integral type.
How to specify it? Syntax-wise it might seem a bit like inheritance! Though there are no access levels to define.
enum Color : int { red, green, blue };
With that you can be sure what the underlying type is. Still, it must be an integral type! For example, it cannot be a string. Should you try that and you'll get a very explicit error message:
main.cpp:4:19: error: underlying type 'std::string' {aka 'std::__cxx11::basic_string<char>'} of 'Color' must be an integral type
4 | enum Color : std::string { red, green, blue };
| ^~~~~~
Note that the core guidelines advocate against this practice! You should only specify the underlying value if it is necessary.
Why can it be necessary? It gives us two reasons.
- If you know that the number of choices will be very limited and you want to save a bit of memory:
enum Direction : char { north, south, east, west,
northeast, northwest, southeast, southwest };
- Or if you happen to forward declare an
enumthen you also must declare the type;
enum Direction : char;
void navigate(Direction d);
enum Direction : char { north, south, east, west,
northeast, northwest, southeast, southwest };
You can also specify the exact value of one or all the enumerated values as long as they are constexpr.
enum Color : int { red = 0, green = 1, blue = 2 };
Once again, the guidelines recommends us not to do this, unless it's necessary! Once you start doing it, it's easy to make mistakes and mess it up. We can rely on the compiler assigning subsequent values to the subsequent enumerator values.
- A good reason to specify the enumerator value is to define only the starting value. If you define the months and you don't want to start with zero.
enum Month { jan = 1, feb, mar, apr, may, jun,
jul, august, sep, oct, nov, dec };
- Another reason can be if you want to define the values as some meaningful character
enum altitude: char {
high = 'h',
low = 'l'
};
- One other reason can be emulating some bitfields. So you don't want subsequent values, but you always want the next power of two
enum access_type { read = 1, write = 2, exec = 4 };
Scoped enumerations
In the previous section, we saw declarations such as enum EnumName{};. C++11 brought a new type of enumeration called scoped enums. They declared either with the class or with the struct keywords and there is no difference between those two.
The syntax is the following:
enum class Color { red, green, blue };