From 04820018751ec4b1077a0a23296273884c0b281a Mon Sep 17 00:00:00 2001 From: Victor Zverovich Date: Sat, 26 Sep 2026 10:56:45 -0700 Subject: [PATCH] Format enums annotated with fmt::as_underlying Format an enum as its underlying value if the enum is annotated with fmt::as_underlying, the equivalent of std::format_as_underlying from P4267: enum class [[=fmt::as_underlying]] color { red = 1, green = 2, blue = 4 }; fmt::format("{:04x}", color::blue); // "0004" The value is mapped before type erasure via the existing formatter::format_as hook, as in the std::byte formatter, so it is stored as a builtin argument and can be used as dynamic width or precision. Combining the annotation with fmt::as_identifiers is rejected with a static_assert. --- doc/api.md | 16 +++++++++++- include/fmt/enum.h | 62 ++++++++++++++++++++++++++++++++++++++++++---- test/enum-test.cc | 27 ++++++++++++++++++++ 3 files changed, 99 insertions(+), 6 deletions(-) diff --git a/doc/api.md b/doc/api.md index 4a49cdc4..c46304e0 100644 --- a/doc/api.md +++ b/doc/api.md @@ -584,7 +584,21 @@ as its underlying value in decimal before applying string formatting: fmt::print("{}", static_cast(42)); // Output: 42 -Enums without the annotation are not affected and are formatted as before, i.e. +An enum annotated with `fmt::as_underlying` is instead formatted as its +underlying value using the integer [Format Specification]( +syntax.md#format-specification): + + enum class [[=fmt::as_underlying]] color { red = 1, green = 2, blue = 4 }; + + fmt::print("{:04x}", color::blue); + // Output: 0004 + +Such an enum is mapped to its underlying type before type erasure, so it can +also be used as dynamic width or precision. Unlike `fmt::as_identifiers`, it +works with all character types. The two annotations specify different +representations and cannot be combined. + +Enums without an annotation are not affected and are formatted as before, i.e. scoped enums require `format_as` or a `formatter` specialization, see [Formatting User-Defined Types](#udt). diff --git a/include/fmt/enum.h b/include/fmt/enum.h index ad3a27cd..0c4640e6 100644 --- a/include/fmt/enum.h +++ b/include/fmt/enum.h @@ -33,7 +33,6 @@ FMT_BEGIN_NAMESPACE #if FMT_USE_REFLECTION -/// The type of the `fmt::as_identifiers` annotation. FMT_EXPORT struct as_identifiers_t {}; /** @@ -49,19 +48,56 @@ FMT_EXPORT struct as_identifiers_t {}; */ FMT_EXPORT inline constexpr auto as_identifiers = as_identifiers_t(); +FMT_EXPORT struct as_underlying_t {}; + +/** + * An annotation that makes an enum format as its underlying value. + * + * **Example**: + * + * enum class [[=fmt::as_underlying]] color { red = 1, green = 2 }; + * auto s = fmt::format("{:04x}", color::green); // s == "0002" + */ +FMT_EXPORT inline constexpr auto as_underlying = as_underlying_t(); + namespace detail { +// Returns true if U is annotated with an annotation of type A. +template consteval auto is_annotated() -> bool { + return !std::meta::annotations_of_with_type(^^U, ^^A).empty(); +} + // Returns true if T is an enum annotated with fmt::as_identifiers. template > consteval auto use_identifiers() -> bool { - if constexpr (!std::is_enum::value) { - return false; + if constexpr (std::is_enum::value) { + return is_annotated(); } else { - return !std::meta::annotations_of_with_type(^^U, ^^as_identifiers_t) - .empty(); + return false; } } +// Returns true if T is an enum annotated with fmt::as_underlying. +template > +consteval auto use_underlying() -> bool { + if constexpr (std::is_enum::value) { + // The annotations specify different representations. Reporting this here + // gives a better diagnostic than an ambiguous formatter specialization. + static_assert(!is_annotated() || + !is_annotated(), + "fmt::as_underlying and fmt::as_identifiers are mutually " + "exclusive"); + return is_annotated(); + } else { + return false; + } +} + +// The formatter for the underlying type of E. formatter is only specialized +// for mapped types, e.g. unsigned rather than unsigned char. +template +using underlying_formatter = formatter, Char>, Char>; + // Returns the underlying value of `value` converted to uint64_t. Negative // values wrap around, so the difference of two such values is the distance // between them. @@ -208,6 +244,22 @@ struct formatter()>> { } }; +// A formatter for enums annotated with fmt::as_underlying. +template +struct formatter()>> + : detail::underlying_formatter { + FMT_CONSTEXPR static auto format_as(E value) noexcept -> underlying_t { + return underlying(value); + } + + template + FMT_CONSTEXPR auto format(E value, FormatContext& ctx) const + -> decltype(ctx.out()) { + using base = detail::underlying_formatter; + return base::format(format_as(value), ctx); + } +}; + #endif // FMT_USE_REFLECTION FMT_END_NAMESPACE diff --git a/test/enum-test.cc b/test/enum-test.cc index b424ef78..314e0de5 100644 --- a/test/enum-test.cc +++ b/test/enum-test.cc @@ -73,6 +73,10 @@ enum class [[=fmt::as_identifiers]] extremes : int { enum class [[=fmt::as_identifiers]] big : unsigned long long { huge = ULLONG_MAX }; + +enum class [[=fmt::as_underlying]] flags { red = 1, green = 2, blue = 4 }; +enum class [[=fmt::as_underlying]] small_flags : unsigned char { one = 1 }; +enum class [[=fmt::as_underlying]] size : int { large = 10 }; // clang-format on TEST(enum_test, format_enum) { @@ -246,4 +250,27 @@ TEST(enum_test, annotation_is_required) { EXPECT_TRUE(fmt::is_formattable::value); EXPECT_FALSE(fmt::is_formattable::value); } + +TEST(enum_test, format_underlying) { + EXPECT_EQ(fmt::format("{}", flags::green), "2"); + // Integer format specifiers are accepted unlike for as_identifiers. + EXPECT_EQ(fmt::format("{:04x}", flags::blue), "0004"); + // A one-byte underlying type is written in decimal, not as a character. + EXPECT_EQ(fmt::format("{}", small_flags::one), "1"); +} + +TEST(enum_test, format_underlying_is_mapped) { + // The value is mapped to its underlying type before type erasure. + static_assert(fmt::detail::mapped_type_constant::value == + fmt::detail::type::int_type); + static_assert(fmt::detail::mapped_type_constant::value == + fmt::detail::type::uint_type); + // Mapping to an integer allows using an enum as dynamic width. + EXPECT_EQ(fmt::format("{:{}}", 42, size::large), " 42"); +} + +TEST(enum_test, format_underlying_range) { + auto v = std::vector{flags::red, flags::blue}; + EXPECT_EQ(fmt::format("{}", v), "[1, 4]"); +} #endif // FMT_USE_REFLECTION