-
Book Overview & Buying
-
Table Of Contents
Modern C++ Programming Cookbook - Second Edition
By :
Enumeration is a basic type in C++ that defines a collection of values, always of an integral underlying type. Their named values, which are constant, are called enumerators. Enumerations declared with the keyword enum are called unscoped enumerations, while enumerations declared with enum class or enum struct are called scoped enumerations. The latter ones were introduced in C++11 and are intended to solve several problems with unscoped enumerations, which are explained in this recipe.
When working with enumerations, you should:
enum class or enum struct:
enum class Status { Unknown, Created, Connected };
Status s = Status::Created;
The enum class and enum struct declarations are equivalent, and throughout this recipe and the rest of this book, we will use enum class.
Because scope enumerations are restricted namespaces, the C++20 standard allows us to associate them with a using directive. You can do the following:
using directive, as follows:
int main()
{
using Status::Unknown;
Status s = Unknown;
}
using directive, as follows:
struct foo
{
enum class Status { Unknown, Created, Connected };
using enum Status;
};
foo::Status s = foo::Created; // instead of
// foo::Status::Created
using enum directive to introduce the enum identifiers in a switch statement to simplify your code:
void process(Status const s)
{
switch (s)
{
using enum Status;
case Unknown: /*…*/ break;
case Created: /*...*/ break;
case Connected: /*...*/ break;
}
}
Converting a scoped enumeration to its underlying type is sometimes necessary, especially in the context of using old-style APIs that take integers as arguments. In C++23, you can convert to the underlying type of a scoped enumeration by using the std::to_underlying() utility function:
void old_api(unsigned flag);
enum class user_rights : unsigned
{
None, Read = 1, Write = 2, Delete = 4
};
old_api(std::to_underlying(user_rights::Read));
Unscoped enumerations have several issues that create problems for developers:
enum Status {Unknown, Created, Connected};
enum Codes {OK, Failure, Unknown}; // error
auto status = Status::Created; // error
int, unless the enumerator value cannot fit a signed or unsigned integer. Owing to this, forward declaration of enumerations was not possible. The reason for this was that the size of the enumeration was not known. This was because the underlying type was not known until the values of the enumerators were defined so that the compiler could pick the appropriate integer type. This has been fixed in C++11.int. This means you can intentionally or accidentally mix enumerations that have a certain meaning and integers (which may not even be related to the meaning of the enumeration) and the compiler will not be able to warn you:
enum Codes { OK, Failure };
void include_offset(int pixels) {/*...*/}
include_offset(Failure);
The scoped enumerations are basically strongly typed enumerations that behave differently than the unscoped enumerations:
enum class Status { Unknown, Created, Connected };
enum class Codes { OK, Failure, Unknown }; // OK
Codes code = Codes::Unknown; // OK
enum class Codes : unsigned int;
void print_code(Codes const code) {}
enum class Codes : unsigned int
{
OK = 0,
Failure = 1,
Unknown = 0xFFFF0000U
};
int. Assigning the value of an enum class to an integer variable would trigger a compiler error unless an explicit cast is specified:
Codes c1 = Codes::OK; // OK
int c2 = Codes::Failure; // error
int c3 = static_cast<int>(Codes::Failure); // OK
However, the scoped enumerations have a drawback: they are restricted namespaces. They do not export the identifiers in the outer scope, which can be inconvenient at times, for instance, if you are writing a switch and you need to repeat the enumeration name for each case label, as in the following example:
std::string_view to_string(Status const s)
{
switch (s)
{
case Status::Unknown: return "Unknown";
case Status::Created: return "Created";
case Status::Connected: return "Connected";
}
}
In C++20, this can be simplified with the help of a using directive with the name of the scoped enumeration. The preceding code can be simplified as follows:
std::string_view to_string(Status const s)
{
switch (s)
{
using enum Status;
case Unknown: return "Unknown";
case Created: return "Created";
case Connected: return "Connected";
}
}
The effect of this using directive is that all the enumerator identifiers are introduced in the local scope, making it possible to refer to them with the unqualified form. It is also possible to bring only a particular enum identifier to the local scope with a using directive with the qualified identifier name, such as using Status::Connected.
The C++23 version of the standard adds a couple of utility functions for working with scoped enumerations. The first of these is std::to_underlying(), available in the <utility> header. What it does is convert an enumeration to its underlying type.
Its purpose is to work with APIs (legacy or not) that don’t use scoped enumerations. Let’s look at an example. Consider the following function, old_api(), which takes an integer argument, which it interprets as flags controlling user rights, into the system:
void old_api(unsigned flag)
{
if ((flag & 0x01) == 0x01) { /* can read */ }
if ((flag & 0x02) == 0x02) { /* can write */ }
if ((flag & 0x04) == 0x04) { /* can delete */ }
}
This function can be invoked as follows:
old_api(1); // read only
old_api(3); // read & write
Conversely, a newer part of the system defines the following scoped enumeration for the user rights:
enum class user_rights : unsigned
{
None,
Read = 1,
Write = 2,
Delete = 4
};
However, invoking the old_api() function with enumerations from user_rights is not possible, and a static_cast must be used:
old_api(static_cast<int>(user_rights::Read)); // read only
old_api(static_cast<int>(user_rights::Read) |
static_cast<int>(user_rights::Write)); // read & write
To avoid these static casts, C++23 provides the function std::to_underlying(), which can be used as follows:
old_api(std::to_underlying(user_rights::Read));
old_api(std::to_underlying(user_rights::Read) |
std::to_underlying(user_rights::Write));
The other utility introduced in C++23 is a type trait called is_scoped_enum<T>, available in the <type_traits> header. This contains a member constant called value, which is equal to true if the template type parameter T is a scoped enumeration type, or false otherwise. There is also a helper variable template, is_scoped_enum_v<T>.
The purpose of this type trait is to identify whether an enumeration is scoped or not in order to apply different behavior, depending on the type of the enumeration. Here is a simple example:
enum A {};
enum class B {};
int main()
{
std::cout << std::is_scoped_enum_v<A> << '\n';
std::cout << std::is_scoped_enum_v<B> << '\n';
}
The first line will print 0 because A is an unscoped enum, while the second line will print 1 because B is a scoped enum.