Reader small image

You're reading from  Modern C++ Programming Cookbook - Third Edition

Product typeBook
Published inFeb 2024
PublisherPackt
ISBN-139781835080542
Edition3rd Edition
Right arrow
Author (1)
Marius Bancila
Marius Bancila
author image
Marius Bancila

Marius Bancila is a software engineer with two decades of experience in developing solutions for line of business applications and more. He is the author of The Modern C++ Challenge and Template Metaprogramming with C++. He works as a software architect and is focused on Microsoft technologies, mainly developing desktop applications with C++ and C#. He is passionate about sharing his technical expertise with others and, for that reason, he has been recognized as a Microsoft MVP for C++ and later developer technologies since 2006. Marius lives in Romania and is active in various online communities.
Read more about Marius Bancila

Right arrow

Using scoped enumerations

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.

How to do it...

When working with enumerations, you should:

  • Prefer to use scoped enumerations instead of unscoped ones
  • Declare scoped enumerations using 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:

  • Introduce a scoped enumeration identifier in the local scope with a using directive, as follows:
    int main()
    {
      using Status::Unknown;
      Status s = Unknown;
    }
    
  • Introduce all the identifiers of a scoped enumeration in the local scope with a 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
    
  • Use a 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));

How it works...

Unscoped enumerations have several issues that create problems for developers:

  • They export their enumerators to the surrounding scope (for which reason, they are called unscoped enumerations), and that has the following two drawbacks:
    • It can lead to name clashes if two enumerations in the same namespace have enumerators with the same name
    • It’s not possible to use an enumerator using its fully qualified name:
      enum Status {Unknown, Created, Connected};
      enum Codes {OK, Failure, Unknown};   // error
      auto status = Status::Created;       // error
      
  • Prior to C++ 11, they could not specify the underlying type, which is required to be an integral type. This type must not be larger than 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.
  • Values of enumerators implicitly convert to 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:

  • They do not export their enumerators to the surrounding scope. The two enumerations shown earlier would change to the following, no longer generating a name collision and making it possible to fully qualify the names of the enumerators:
    enum class Status { Unknown, Created, Connected };
    enum class Codes { OK, Failure, Unknown }; // OK
    Codes code = Codes::Unknown;               // OK
    
  • You can specify the underlying type. The same rules for underlying types of unscoped enumerations apply to scoped enumerations too, except that the user can explicitly specify the underlying type. This also solves the problem with forward declarations, since the underlying type can be known before the definition is available:
    enum class Codes : unsigned int;
    void print_code(Codes const code) {}
    enum class Codes : unsigned int
    {
      OK = 0,
      Failure = 1,
      Unknown = 0xFFFF0000U
    };
    
  • Values of scoped enumerations no longer convert implicitly to 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.

See also

  • Chapter 9, Creating compile-time constant expressions, to learn how to work with compile-time constants
Previous PageNext Page
You have been reading a chapter from
Modern C++ Programming Cookbook - Third Edition
Published in: Feb 2024Publisher: PacktISBN-13: 9781835080542
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €14.99/month. Cancel anytime

Author (1)

author image
Marius Bancila

Marius Bancila is a software engineer with two decades of experience in developing solutions for line of business applications and more. He is the author of The Modern C++ Challenge and Template Metaprogramming with C++. He works as a software architect and is focused on Microsoft technologies, mainly developing desktop applications with C++ and C#. He is passionate about sharing his technical expertise with others and, for that reason, he has been recognized as a Microsoft MVP for C++ and later developer technologies since 2006. Marius lives in Romania and is active in various online communities.
Read more about Marius Bancila