Table of Contents

The evolution of the C++ language continues to bring powerful features that enhance code safety, readability, and maintainability. Among these improvements, we got changes and additions to enum class functionalities across C++17, C++20, and C++23. In this blog post, we’ll explore these advancements, focusing on initialization improvements in C++17, the introduction of the using enum keyword in C++20, and the std::to_underlying utility in C++23.

Let’s go.

Enum Class Recap  

Before diving into the enhancements, let’s briefly recap what enum class is. An enum class (scoped enumeration) provides a type-safe way of defining a set of named constants. Unlike traditional (unscoped) enums, enum class does not implicitly convert to integers or other types, preventing accidental misuse. Here’s a basic example:

#include <iostream>

enum class Color {
    Red,
    Green,
    Blue
};

int main() {    
    Color color = Color::Red;

    if (color == Color::Red)
        std::cout << "The color is red.\n";

    color = Color::Blue;

    if (color == Color::Blue)
        std::cout << "The color is blue.\n";

    // std::cout << color; // error, no matching << operator
    // int i = color;      // error: cannot convert
}

Run @Compiler Explorer

Notice the two lines near the end of the main function. You’ll get compiler errors as there’s no implicit conversion to integer types.

As a comparison here’s a similar example, but with unscoped enums:

#include <iostream>

enum Color {
    Red,
    Green,
    Blue
};

int main() {    
    Color color = Red;

    if (color == Red)
        std::cout << "The color is red.\n";

    color = Blue;

    if (color == Blue)
        std::cout << "The color is blue.\n";

    std::cout << color; // fine, prints integer value!
    int i = color;      // fine, can convert...
}

Run @Compiler Explorer

In short, while enum classes give us a separate scope for all of the enum values, they also enforce better type safety. There are no implicit conversions to integral values, and thus, you have better control over your design.

While the basics are simple, let’s now go to several improvements that are handy in the latest C++ revisions.

C++17: Initialization with Brace Initialization from Underlying Type  

Enum class might sometimes feel too restrictive, and having some conversions might be handy:

In P0138 accepted for C++17 we have the following example:

enum class Handle : uint32_t { Invalid = 0 }; 
Handle h { 42 }; // OK

In short, when you use enum class to define strong types, it’s helpful to allow initializing from the underlying type without any errors. This wasn’t possible before C++17.

However, the change was cautious so that enums are still safe - they can be used only for uniform/brace initialization. Have a look at this code:

#include <iostream>

enum class Handle : uint32_t { Invalid = 0 }; 

void process(Handle h) {

}

int main() {    
    Handle h { 42 }; // OK

    // process({10}); // error
    process(Handle{10});
}

You cannot just pass {10} as the parameter for the process function. You still have to be explicit about the type.

In C++14, you could use process(static_cast<Handle>(10)); so, as you can see, the C++17 version is much better.

C++20: Using Enum  

C++20 introduced the using enum syntax. This feature allows you to bring all the enumerators of an enum into the current scope without losing the benefits of scoped enums.

Consider the following example:

enum class ComputeStatus {
    Ok,
    Error,
    FileError,
    NotEnoughMemory,
    TimeExceeded,
    Unknown
};

In previous C++ versions, using these enumerators required qualifying them with the enum class name:

ComputeStatus s = ComputeStatus::NotEnoughMemory;

C++20 simplifies this with the using enum declaration:

int main() {
    using enum ComputeStatus;
    ComputeStatus s = NotEnoughMemory;
}

It’s probably no sense in this silly code above, but how about the following case:

int main() {    
    ComputeStatus s = ComputeStatus::Ok;
    switch (s) {
        case ComputeStatus::Ok: 
            std::cout << "ok"; break;
        case ComputeStatus::Error: 
            std::cout << "Error"; break;
        case ComputeStatus::FileError: 
            std::cout << "FileError"; break;
        case ComputeStatus::NotEnoughMemory: 
            std::cout << "NotEnoughMemory"; break;
        case ComputeStatus::TimeExceeded: 
            std::cout << "Time..."; break;
        default: std::cout << "unknown...";
    }
}

We can convert it into:

int main() {    
    ComputeStatus s = ComputeStatus::Ok;
    switch (s) {
        using enum ComputeStatus;	// << <<
        case Ok: 
            std::cout << "ok"; break;
        case Error: 
            std::cout << "Error"; break;
        case FileError: 
            std::cout << "FileError"; break;
        case NotEnoughMemory: 
            std::cout << "NotEnoughMemory"; break;
        case TimeExceeded: 
            std::cout << "Time..."; break;
        default: std::cout << "unknown...";
    }
}

Run @Compiler Explorer

Or have a look at the following example:

struct ComputeEngine {
    enum class ComputeStatus {
        Ok,
        Error,
        FileError,
        NotEnoughMemory,
        TimeExceeded,
        Unknown
    };
    using enum ComputeStatus;
};

int main() {    
    ComputeEngine::ComputeStatus s = ComputeEngine::Ok;
}

You can bring all enumerations into the scope of ComputeEngine and still benefit from enum class features.

This C++20 improvement makes the code cleaner and reduces verbosity, especially in cases where multiple enumerators are frequently used within a scope. It allows for a more streamlined and readable approach without sacrificing the type safety provided by scoped enums.

C++23: std::to_underlying  

C++23 further enhances enum class usability with the introduction of std::to_underlying, a utility function that converts an enum value to its underlying integral type. This feature addresses the common need to convert enum values to integers for storage, comparison, or interoperability with APIs that expect integral types.

The idea for this appeared in a wonderful book by Scott Meyers - Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14. Finally in C++23 we can enjoy this feature being standardized.

Before C++23, converting an enum to its underlying type required explicit casting:

enum class Permissions : uint8_t {
    Execute = 1,
	Write = 2,
    Read = 4
};

uint8_t value = static_cast<uint8_t>(Permissions::Read);

With std::to_underlying, this conversion becomes more straightforward and expressive:

#include <type_traits>

int main() {
    Permissions p = Permissions::Read;
    auto value = std::to_underlying(p); // C++23
}

The std::to_underlying function improves code readability and reduces the boilerplate associated with type casting. It also clarifies the intent, making it evident that the purpose is to obtain the underlying value of the enum.

Read more in P1682R0: std::to_underlying for enumerations

If you want to read more about bitmasks, have a look at this cool article by Andreas Fertig: C++20 Concepts applied - Safe bitmasks using scoped enums - Andreas Fertig’s Blog.

Future improvements  

One important update that we may receive soon is support for C++26 reflections - check out P2996 (the proposal has not been accepted yet, but is expected to be approved soon…). Reflection opens up numerous exciting possibilities, such as the ability to convert enums into strings.

See this example from the proposal:

template <typename E>
  requires std::is_enum_v<E>
constexpr std::string enum_to_string(E value) {
  template for (constexpr auto e : std::meta::enumerators_of(^E)) {
    if (value == [:e:]) {
      return std::string(std::meta::name_of(e));
    }
  }
  return "<unnamed>";
}

enum Color { red, green, blue };
static_assert(enum_to_string(Color::red) == "red");
static_assert(enum_to_string(Color(42)) == "<unnamed>");

See at EDG experimental implementation @CompilerExplorer

Of course, you don’t have to wait till C++26 and you can rely on third-party libraries like Neargye/magic_enum @Github. (Thanks to mentioning that in the comment, maxpagani).

Summary  

In this article, we covered some of the new features introduced in C++17, C++20, and C++23 for enum classes. Enum classes provide type safety, but there are situations where we may need to relax some restrictions and write more concise code. We also explored the potential use case for C++26 reflection.

Back to you

  • Have you tried the latest improvements for enum class?
  • Do you use enum class for strong types or some other techniques?

Share your feedback below in the comments.