How to Use fmtlib's enum.h for Compile-Time-Checked Enum Formatting

fmtlib provides compile-time-checked enum formatting via the [[=fmt::as_identifiers]] annotation in include/fmt/enum.h, which generates lookup tables at compile time to print enumerator names instead of integer values when C++23 static reflection is available or FMT_USE_REFLECTION is enabled.

The {fmt} library offers a type-safe mechanism for formatting C++ enumerations as human-readable identifier names rather than raw integers. This feature, implemented in include/fmt/enum.h, leverages static reflection capabilities to validate enum-to-string mappings at compilation time, ensuring that formatting logic never drifts from the actual enum definition.

Prerequisites: C++23 Reflection and Configuration

Compile-time enum formatting requires compiler support for static reflection, indicated by the __cpp_impl_reflection or __cpp_lib_reflection feature test macros. If your compiler lacks C++23 reflection support, the entire header effectively disables itself by defining FMT_USE_REFLECTION as 0. You can manually control this behavior by defining FMT_USE_REFLECTION when building the library to force the feature on or off regardless of compiler capabilities.

Annotating Enums with fmt::as_identifiers

To enable identifier formatting, annotate your enum definition with [[=fmt::as_identifiers]]. This attribute marks the type for compile-time processing by the formatting machinery.

#include <fmt/core.h>
#include <fmt/enum.h>

enum class [[=fmt::as_identifiers]] Color { red, green, blue };

The annotation definition resides in include/fmt/enum.h at lines 38-50. Only annotated enums receive the specialized formatter; unannotated enums fall back to standard integral formatting, making this an explicit opt-in feature that maintains type safety.

How the Compile-Time Checking Works

The implementation uses template metaprogramming and consteval functions to build lookup structures at compile time. This approach eliminates runtime overhead while guaranteeing that every formatted value corresponds to a valid enumerator or triggers the fallback mechanism.

Type Detection with use_identifiers()

At lines 54-63, the detail::use_identifiers<T>() trait checks whether a type is an enum and carries the as_identifiers annotation. This boolean trait controls whether the specialized formatter is instantiated for a given type.

Lookup Table Generation for Dense Enums

For enums with consecutive values (dense), the library generates a flat lookup table via make_identifier_table (lines 100-112). This identifier_table stores string views indexed by the enum's underlying value, providing O(1) lookup with minimal memory overhead.

Hash Map Construction for Sparse Enums

When enum values are sparse or non-consecutive, make_identifier_map (lines 140-156) constructs a compile-time hash map (identifier_map). This structure handles disconnected value ranges efficiently without wasting storage on empty slots.

The identifier_of Lookup Function

The detail::identifier_of(E value) function (lines 164-182) performs the actual resolution. It returns a std::string_view containing the enumerator's identifier if the value exists in the lookup structure, or an empty view if the value does not correspond to a defined enumerator.

Formatter Specialization and Fallback Behavior

At lines 86-100, a partial formatter specialization is enabled for any enum satisfying detail::use_identifiers<E>(). The format() method first attempts to resolve the identifier via identifier_of. If the result is empty (indicating an unknown value), it falls back to formatting the underlying integral value using underlying(value), ensuring that even invalid enum values produce meaningful output.

Practical Implementation Examples

Basic Identifier Formatting

The following example demonstrates formatting an annotated enum directly:

#include <fmt/core.h>
#include <fmt/enum.h>

enum class [[=fmt::as_identifiers]] Color { red, green, blue };

int main() {
    fmt::print("{}\n", Color::green);          // Output: green
    fmt::print("{:04}\n", Color::red);         // Output: red (width applied)
}

Fallback for Unknown Values

When an enum variable contains a value not present in the definition, the formatter falls back to the numeric representation:

enum class [[=fmt::as_identifiers]] ErrorCode { ok = 0, not_found = 404 };

int main() {
    ErrorCode e = static_cast<ErrorCode>(500);   // Undefined enumerator
    fmt::print("{}\n", e);                       // Output: 500
}

This fallback occurs because identifier_of returns an empty view for unmatched values, triggering the integral formatting path at lines 96-100.

Explicit Compile-Time Safety

The formatter is only enabled for annotated enums. Attempting to format a non-annotated enum without a custom formatter will use the generic integral formatter, but if you expect identifier formatting, the absence of the annotation serves as a compile-time signal that the type lacks the required metadata:

enum class BadEnum { a, b };  // Missing annotation

// fmt::format("{}", BadEnum::a);  // Uses integral formatter, not identifier

Direct Lookup Without Formatting

You can access the compile-time lookup machinery directly for static assertions or constexpr contexts:

enum class [[=fmt::as_identifiers]] State { idle, running, stopped };

static_assert(std::string_view(fmt::detail::identifier_of(State::running)) == "running");

This utilizes the constexpr identifier_of function independently of the formatting pipeline.

Summary

  • Compile-time enum formatting in fmtlib requires the [[=fmt::as_identifiers]] annotation and C++23 static reflection support (or FMT_USE_REFLECTION).
  • The implementation in include/fmt/enum.h uses detail::use_identifiers<T>() to detect annotated enums at compile time.
  • Dense enums utilize make_identifier_table (lines 100-112) while sparse enums use make_identifier_map (lines 140-156) for efficient lookup.
  • The formatter specialization (lines 86-100) calls identifier_of to resolve names, falling back to underlying integral values for unknown enumerators.
  • All lookup structures are built using consteval/constexpr machinery, ensuring errors are caught during compilation rather than at runtime.

Frequently Asked Questions

What happens if I format an enum without the fmt::as_identifiers annotation?

Unannotated enums fall back to fmtlib's generic integral formatter, which outputs the underlying numeric value. The specialized identifier formatter is only enabled for types satisfying detail::use_identifiers<E>(), ensuring that identifier formatting is always explicit and opt-in.

Does this feature work with C++17 or C++20 compilers?

No, compile-time-checked enum formatting requires C++23 static reflection capabilities (__cpp_impl_reflection). If your compiler does not support this feature, FMT_USE_REFLECTION defaults to 0 and the header is effectively disabled unless you manually enable the macro and provide the necessary reflection support yourself.

How does fmtlib handle enum values that are not defined in the enumeration?

When a value lacks a defined enumerator, detail::identifier_of returns an empty string_view, causing the formatter to fall back to printing the underlying integral value (as seen at lines 96-100 in enum.h). This ensures robust output even when handling uninitialized or cast enum variables.

Can I use the identifier lookup without the fmt::format function?

Yes, the fmt::detail::identifier_of function is a constexpr utility that can be used in any compile-time context, including static_assert statements or template metaprogramming. This allows you to leverage the compile-time lookup tables independently of the formatting pipeline.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →