How to Format Enums with fmtlib: Identifier Names vs Integer Values

To format enums with fmtlib, annotate your enum with [[=fmt::as_identifiers]] to print enumerator names, or leave it unannotated to print the underlying integer value.

The fmtlib/fmt library supports two distinct modes for enum formatting: default integer output and identifier-based string representation. When you format enums with fmtlib, the library checks for the presence of the fmt::as_identifiers annotation at compile time to determine which formatting strategy to apply. This behavior is implemented in include/fmt/enum.h and leverages C++ reflection facilities when available.

Default Behavior vs Identifier-Based Formatting

fmtlib handles enums through two primary code paths depending on whether you opt into identifier formatting.

Default Behavior: Printing Underlying Integer Values

By default, unannotated enums format as their underlying integral values. The generic formatter specialization calls detail::write on the enum's underlying type, producing numeric output suitable for debugging or logging scenarios where symbolic names are unnecessary.

This fallback mechanism resides in include/fmt/enum.h at lines 99-107, where the formatter handles cases where identifier lookup fails or reflection is unavailable.

Named Enum Formatting with fmt::as_identifiers

To display enumerator names instead of integers, annotate your enum declaration with [[=fmt::as_identifiers]]. This attribute, defined in include/fmt/enum.h (lines 36-50), activates a compile-time formatter that converts enum values to their string identifiers.

The detection occurs through detail::use_identifiers<E>(), a constexpr function (lines 54-62) that checks for the annotation. When present, the formatter retrieves the identifier via detail::identifier_of, which constructs a perfect hash table or open-addressed map from reflection metadata (lines 64-82 and 164-176). If the identifier exists, the formatter delegates to formatter<string_view>; otherwise, it falls back to the integer representation at lines 98-107.

Internal Mechanism of Enum Formatting

The identifier-based system relies on compile-time reflection infrastructure controlled by the FMT_USE_REFLECTION macro.

Compile-Time Detection and Lookup

Annotation detection happens via detail::use_identifiers<E>(), which returns true only for annotated enums. This triggers the construction of constexpr lookup tables through make_identifier_table or make_identifier_map (lines 84-112 and 119-127), creating arrays that map enum values to string literals.

Identifier retrieval uses detail::identifier_of(value) to perform constant-time lookups via direct indexing or open-addressed probing (lines 164-176). The function returns a string_view representing the enumerator name, which the formatter<E, char> then passes to the standard string formatter.

Reflection Requirements

When FMT_USE_REFLECTION is enabled (requiring compiler support and the <version> header), fmtlib exposes the identifier-based formatter. Without reflection support, the library omits this functionality, and all enums format as integers regardless of annotation.

Practical Code Examples

The following examples demonstrate how to format enums with fmtlib in real-world scenarios.

Basic Identifier Formatting

Include fmt/enum.h and apply the annotation to print enum names:

#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::blue);   // Output: blue (width applies to string)
}

Integer Fallback for Unannotated Enums

Without the annotation, fmtlib prints the underlying value:

#include <fmt/core.h>

enum class Status { ok = 0, warning = 1, error = 2 };

int main() {
    fmt::print("{}\n", Status::warning);   // Output: 1
}

Mixed Usage in Same Program

You can combine annotated and unannotated enums within the same format string:

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

enum class [[=fmt::as_identifiers]] State { idle, running, stopped };
enum class Level { low = 10, medium = 20, high = 30 };

int main() {
    fmt::print("State: {}, Level: {}\n",
               State::running,   // prints "running"
               Level::medium); // prints "20"
}

Custom Fallback Handling

For handling potentially invalid enum values safely, wrap the enum and check for empty identifiers:

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

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

struct SafeError {
    ErrorCode code;
};

template <> struct fmt::formatter<SafeError> {
    fmt::formatter<std::string_view> sv;
    constexpr auto parse(fmt::format_parse_context& ctx) { return sv.parse(ctx); }
    
    template <typename Ctx>
    auto format(const SafeError& e, Ctx& ctx) const {
        auto id = fmt::detail::identifier_of(e.code);
        return id.empty() ? sv.format("UNKNOWN", ctx) : sv.format(id, ctx);
    }
};

int main() {
    fmt::print("{}\n", SafeError{static_cast<ErrorCode>(123)}); // Output: UNKNOWN
}

This custom formatter reuses the internal identifier_of function from include/fmt/enum.h while providing application-specific fallback logic.

Summary

  • Default formatting prints the underlying integer value via detail::write when enums lack the fmt::as_identifiers annotation.
  • Identifier formatting requires the [[=fmt::as_identifiers]] attribute and C++ reflection support (FMT_USE_REFLECTION).
  • Lookup mechanism uses constexpr perfect hash tables built by make_identifier_table or make_identifier_map in include/fmt/enum.h.
  • Fallback behavior automatically prints integers when identifier lookup fails or reflection is unavailable.
  • Custom formatters can leverage fmt::detail::identifier_of to implement specialized error handling while maintaining the library's lookup performance.

Frequently Asked Questions

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

fmtlib prints the underlying integral value of the enumerator. The generic formatter in include/fmt/enum.h calls detail::write on the enum's base type, producing numeric output identical to formatting the equivalent integer.

Does identifier-based enum formatting require C++23 reflection?

Yes, the identifier-based functionality requires compiler support for C++ reflection (std::meta), controlled by the FMT_USE_REFLECTION macro. When reflection is unavailable, the library omits the as_identifiers formatter and falls back to integer output even for annotated enums.

Can I apply formatting specifiers like width and precision to enum names?

Yes, when using [[=fmt::as_identifiers]], the formatter delegates to formatter<string_view>, which respects standard string formatting specifiers. Width, precision, and alignment apply to the enumerator name string rather than the numeric value.

How does fmtlib handle invalid or out-of-range enum values?

When an enum value has no corresponding identifier in the compile-time lookup table, detail::identifier_of returns an empty string_view. The formatter then falls back to printing the underlying integer value (lines 98-107 in include/fmt/enum.h). For custom behavior, implement a specialized formatter that checks for empty identifiers before delegating to the default implementation.

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 →