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
- Compile-Time Execution (
constexpr):std::to_underlyingis markedconstexpr, meaning it can be evaluated in constant expressions such as template parameters, array sizes, orstatic_assertconditions. - Exception Safety (
noexcept): Because converting an enumeration to its fundamental representation is a zero-cost cast at the hardware level, the function is guaranteednoexcept. - 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.