How C++ std::underlying_type Determines Enum Backing Types

In C++, std::underlying_type is a standard type trait that extracts the underlying integral type of an enumeration at compile time. This article explains the mechanism behind std::underlying_type, detailing how the compiler establishes the numeric backing type of scoped and unscoped enums, and how standard library implementations leverage compiler intrinsics to expose this type to developers.

The Role of Compiler Intrinsics

At the library level, std::underlying_type cannot deduce an enumeration's backing type using pure template metaprogramming or pattern matching. Because the mapping between an enum and its underlying integer representation is resolved directly inside the compiler's semantic analysis phase, the standard library delegates this work to a compiler intrinsic.

In major toolchains like GCC, Clang, and MSVC, the implementation of std::underlying_type resembles the following:

template <typename T>
struct underlying_type {
    using type = __underlying_type(T);
};

template <typename T>
using underlying_type_t = typename underlying_type<T>::type;

The intrinsic __underlying_type(T) queries the compiler's internal Abstract Syntax Tree (AST) for the symbol definition of T, verifies that T is a complete enumeration type, and immediately returns the designated integer type.

How the Compiler Establishes the Underlying Type

To provide the intrinsic with the correct type, the compiler assigns an underlying integral type during the declaration of the enumeration based on ISO C++ rules:

  1. Scoped Enumerations (enum class and enum struct)
    • Explicit type: If specified (e.g., enum class Status : uint8_t), the underlying type is strictly that type.
    • Implicit default: If no type is specified (e.g., enum class Status), the standard dictates that the underlying type defaults directly to int.
  2. Unscoped Enumerations with an Explicit Type
    • If an unscoped enum specifies a type (e.g., enum Flags : unsigned long), the underlying type is fixed to that specification, matching the behavior of scoped enums.
  3. Unscoped Enumerations without an Explicit Type
    • When an unscoped enum omits an explicit base (e.g., enum Color { Red, Green, Blue }), the underlying type is not fixed until all enumerators are evaluated.
    • The compiler analyzes the smallest and largest values among the enumerators.
    • It then selects an implementation-defined integral type capable of representing all defined enumerator values. Typically, the compiler checks types in an order such as int, unsigned int, long, unsigned long, long long, or unsigned long long.
    • If the values fit within int or unsigned int, one of those is chosen according to the signs of the values.

Constraints and Validity

Before resolving the type, the compiler verifies that the template argument satisfies the requirements of std::underlying_type:

  • Completeness: The enumeration must be fully defined. Applying std::underlying_type to a forward-declared enum without a fixed underlying type results in a compile-time error.
  • Type Category: The type must be an enumeration. Passing non-enum types (such as classes, pointers, or primitive integers) produces a compilation failure within the intrinsic evaluation.