Converting Enums with std::to_underlying in C++

This article explores std::to_underlying, a utility function introduced in C++23 that simplifies the process of converting an enumeration value to its underlying integral type. It covers the historical problem of casting enums in prior standard revisions, demonstrates the modern syntax and implementation mechanism of std::to_underlying, and explains the practical benefits regarding type safety, readability, and compile-time evaluation.

The Problem Before C++23

Scoped enumerations (enum class) introduced in C++11 provide strong type safety by preventing implicit conversions to integers. However, when interfacing with low-level APIs, serialization routines, or bitwise storage, developers frequently need to extract the raw underlying integer value.

Prior to C++23, obtaining this integral value required an explicit static_cast combined with the std::underlying_type type trait:

enum class Status : uint8_t {
    Pending = 0,
    Approved = 1,
    Rejected = 2
};

Status s = Status::Approved;

// Verbose pre-C++23 conversion
auto value = static_cast<std::underlying_type_t<Status>>(s);

This approach has distinct downsides:

  • Verbosity: Writing static_cast<std::underlying_type_t<EnumType>>(value) is redundant and cluttering.
  • Refactoring Overhead: If the variable name or enum type changes, the cast expression often requires manual updating.

How std::to_underlying Works

Defined in the <utility> header, std::to_underlying eliminates boilerplate by using template argument deduction to automatically resolve the underlying type.

Its standard implementation is conceptually straightforward:

namespace std {
    template <class Enum>
    constexpr underlying_type_t<Enum> to_underlying(Enum e) noexcept {
        return static_cast<underlying_type_t<Enum>>(e);
    }
}

The function accepts any enumeration type (both scoped enum class and unscoped enum), queries its underlying type using std::underlying_type_t, and executes the static_cast internally.

Usage Example

Using std::to_underlying reduces the syntax to a single function call without requiring the enumeration's type name to be written:

#include <iostream>
#include <utility>
#include <cstdint>

enum class HttpCode : uint16_t {
    Ok = 200,
    NotFound = 404,
    InternalError = 500
};

int main() {
    HttpCode code = HttpCode::NotFound;

    // C++23 conversion
    auto raw_value = std::to_underlying(code);

    std::cout << "HTTP Status: " << raw_value << '\n'; // Outputs 404
    static_assert(std::is_same_v<decltype(raw_value), uint16_t>);
}

Key Technical Properties

  1. Compile-Time Execution (constexpr): std::to_underlying is marked constexpr, meaning it can be evaluated in constant expressions such as template parameters, array sizes, or static_assert conditions.
  2. Exception Safety (noexcept): Because converting an enumeration to its fundamental representation is a zero-cost cast at the hardware level, the function is guaranteed noexcept.
  3. Type Constraints: The standard template requires that the passed argument is an enum type. Passing non-enum types results in a clean compile-time substitution failure rather than obscure type errors.