Book Image

Modern C++ Programming Cookbook - Third Edition

By : Marius Bancila
Book Image

Modern C++ Programming Cookbook - Third Edition

By: Marius Bancila

Overview of this book

The updated third edition of Modern C++ Programming Cookbook addresses the latest features of C++23, such as the stack library, the expected and mdspan types, span buffers, formatting library improvements, and updates to the ranges library. It also gets into more C++20 topics not previously covered, such as sync output streams and source_location. The book is organized in the form of practical recipes covering a wide range of real-world problems. It gets into the details of all the core concepts of modern C++ programming, such as functions and classes, iterators and algorithms, streams and the file system, threading and concurrency, smart pointers and move semantics, and many others. You will cover the performance aspects of programming in depth, and learning to write fast and lean code with the help of best practices. You will explore useful patterns and the implementation of many idioms, including pimpl, named parameter, attorney-client, and the factory pattern. A chapter dedicated to unit testing introduces you to three of the most widely used libraries for C++: Boost.Test, Google Test, and Catch2. By the end of this modern C++ programming book, you will be able to effectively leverage the features and techniques of C++11/14/17/20/23 programming to enhance the performance, scalability, and efficiency of your applications.
Table of Contents (15 chapters)
13
Other Books You May Enjoy
14
Index

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