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<T>::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.
This commit is contained in:
Victor Zverovich 2026-09-26 10:56:45 -07:00
parent 522e2c12ab
commit 0482001875
3 changed files with 99 additions and 6 deletions

View File

@ -584,7 +584,21 @@ as its underlying value in decimal before applying string formatting:
fmt::print("{}", static_cast<color>(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).

View File

@ -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 <typename A, typename U> 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 <typename T, typename U = remove_cvref_t<T>>
consteval auto use_identifiers() -> bool {
if constexpr (!std::is_enum<U>::value) {
return false;
if constexpr (std::is_enum<U>::value) {
return is_annotated<as_identifiers_t, U>();
} 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 <typename T, typename U = remove_cvref_t<T>>
consteval auto use_underlying() -> bool {
if constexpr (std::is_enum<U>::value) {
// The annotations specify different representations. Reporting this here
// gives a better diagnostic than an ambiguous formatter specialization.
static_assert(!is_annotated<as_underlying_t, U>() ||
!is_annotated<as_identifiers_t, U>(),
"fmt::as_underlying and fmt::as_identifiers are mutually "
"exclusive");
return is_annotated<as_underlying_t, U>();
} 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 <typename E, typename Char>
using underlying_formatter = formatter<mapped_t<underlying_t<E>, 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<E, char, enable_if_t<detail::use_identifiers<E>()>> {
}
};
// A formatter for enums annotated with fmt::as_underlying.
template <typename E, typename Char>
struct formatter<E, Char, enable_if_t<detail::use_underlying<E>()>>
: detail::underlying_formatter<E, Char> {
FMT_CONSTEXPR static auto format_as(E value) noexcept -> underlying_t<E> {
return underlying(value);
}
template <typename FormatContext>
FMT_CONSTEXPR auto format(E value, FormatContext& ctx) const
-> decltype(ctx.out()) {
using base = detail::underlying_formatter<E, Char>;
return base::format(format_as(value), ctx);
}
};
#endif // FMT_USE_REFLECTION
FMT_END_NAMESPACE

View File

@ -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<color>::value);
EXPECT_FALSE(fmt::is_formattable<color_without_annotation>::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<flags, char>::value ==
fmt::detail::type::int_type);
static_assert(fmt::detail::mapped_type_constant<small_flags, char>::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>{flags::red, flags::blue};
EXPECT_EQ(fmt::format("{}", v), "[1, 4]");
}
#endif // FMT_USE_REFLECTION