From 24d2aaaedbaaa57100d074f259616ae1a1217fe8 Mon Sep 17 00:00:00 2001 From: Avi Kivity Date: Thu, 13 Aug 2026 15:06:15 +0300 Subject: [PATCH] Add fmt/reflect.h and format enums annotated with fmt::as_identifiers Format an enum as the identifier of the matching enumerator if the enum is annotated with fmt::as_identifiers: enum class [[=fmt::as_identifiers]] color { red, green, blue }; fmt::format("{}", color::green); // "green" Identifiers are retrieved via C++26 reflection (P2996) and the annotation via P3394. FMT_USE_REFLECTION is autodetected and can be overridden by the user; without reflection the header is empty. Values that don't match any enumerator are formatted as values of the underlying type. Co-Authored-By: Claude Opus 5 (1M context) --- CMakeLists.txt | 1 + doc/api.md | 41 ++++++ include/fmt/reflect.h | 121 ++++++++++++++++++ .../mkdocstrings_handlers/cxx/__init__.py | 1 + test/CMakeLists.txt | 35 +++++ test/reflect-test.cc | 72 +++++++++++ 6 files changed, 271 insertions(+) create mode 100644 include/fmt/reflect.h create mode 100644 test/reflect-test.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index 5179ce6b3ee0..f3b0697d742f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -281,6 +281,7 @@ foreach ( ostream.h printf.h ranges.h + reflect.h std.h xchar.h) set(FMT_HEADERS ${FMT_HEADERS} include/fmt/${header}) diff --git a/doc/api.md b/doc/api.md index 7ea03e260656..0ec526473b48 100644 --- a/doc/api.md +++ b/doc/api.md @@ -9,6 +9,7 @@ The {fmt} library API consists of the following components: - [`fmt/ranges.h`](#ranges-api): formatting of ranges and tuples - [`fmt/chrono.h`](#chrono-api): date and time formatting - [`fmt/std.h`](#std-api): formatters for standard library types +- [`fmt/reflect.h`](#reflect-api): formatting based on C++26 reflection - [`fmt/compile.h`](#compile-api): format string compilation - [`fmt/color.h`](#color-api): terminal colors and text styles - [`fmt/os.h`](#os-api): system APIs @@ -556,6 +557,46 @@ fmt::print("{}", +s.bit); This is a known limitation of "perfect" forwarding in C++. + +## Reflection-Based Formatting + +`fmt/reflect.h` provides formatting based on C++26 reflection ([P2996]( +https://wg21.link/p2996)) and annotations ([P3394](https://wg21.link/p3394)). +It requires a compiler with reflection support, which may need an extra flag +such as `-freflection` in GCC. The macro `FMT_USE_REFLECTION` is set to 1 if +reflection is available and to 0 otherwise. It can also be defined by the user +to disable the use of reflection. + +An enum annotated with `fmt::as_identifiers` is formatted as the identifier of +the enumerator matching the formatted value: + + #include + + enum class [[=fmt::as_identifiers]] color { red, green, blue }; + + fmt::print("{}", color::green); + // Output: green + +Such enums are formatted using the string [Format Specification]( +syntax.md#format-specification), for example: + + fmt::print("[{:>7}]", color::red); + // Output: [ red] + +Identifiers are only available as `char` strings so annotated enums are not +formattable with other character types. + +If several enumerators have the same value, the first one in the order of +declaration is used. A value that doesn't match any enumerator is formatted as +the corresponding value of the underlying type: + + fmt::print("{}", static_cast(42)); + // Output: 42 + +Enums without the 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). + ## Compile-Time Support diff --git a/include/fmt/reflect.h b/include/fmt/reflect.h new file mode 100644 index 000000000000..1f09809c08de --- /dev/null +++ b/include/fmt/reflect.h @@ -0,0 +1,121 @@ +// Formatting library for C++ - formatting based on C++26 reflection +// +// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors +// All rights reserved. +// +// For the license information refer to format.h. + +#ifndef FMT_REFLECT_H_ +#define FMT_REFLECT_H_ + +#include "format.h" + +#if FMT_HAS_INCLUDE() +# include +#endif + +#ifdef FMT_USE_REFLECTION +// Use the provided definition. +#elif defined(__cpp_impl_reflection) && defined(__cpp_lib_reflection) && \ + defined(__cpp_lib_define_static) +# define FMT_USE_REFLECTION 1 +#else +# define FMT_USE_REFLECTION 0 +#endif + +#if FMT_USE_REFLECTION && !defined(FMT_MODULE) +# include +# include +# include // std::pair +#endif + +FMT_BEGIN_NAMESPACE + +#if FMT_USE_REFLECTION + +/// The type of the `fmt::as_identifiers` annotation. +struct as_identifiers_t {}; + +/** + * An annotation that makes an enum format as identifiers of its enumerators. + * + * **Example**: + * + * enum class [[=fmt::as_identifiers]] color { red, green, blue }; + * auto s = fmt::format("{}", color::green); // s == "green" + * + * A value that doesn't match any enumerator is formatted as the corresponding + * value of the underlying type. + */ +inline constexpr auto as_identifiers = as_identifiers_t(); + +namespace detail { + +// 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; + } else { + return !std::meta::annotations_of_with_type(^^U, ^^as_identifiers_t) + .empty(); + } +} + +template consteval auto count_enumerators() -> size_t { + return std::meta::enumerators_of(^^E).size(); +} + +template ()> +consteval auto make_identifiers() -> std::array, N> { + auto ids = std::array, N>(); + auto i = size_t(); + for (std::meta::info e : std::meta::enumerators_of(^^E)) { + auto id = std::meta::identifier_of(e); + ids[i++] = {std::meta::extract(e), + string_view(std::define_static_string(id), id.size())}; + } + return ids; +} + +// Identifiers of enumerators of E in the order of declaration. +template inline constexpr auto identifiers = make_identifiers(); + +// Returns the identifier of the first enumerator of E equal to value or an +// empty string view if there is no such enumerator. +template constexpr auto identifier_of(E value) -> string_view { + for (const auto& id : identifiers) { + if (id.first == value) return id.second; + } + return {}; +} + +} // namespace detail + +// A formatter for enums annotated with fmt::as_identifiers. +template +struct formatter()>> { + private: + formatter impl_; + + public: + FMT_CONSTEXPR auto parse(parse_context& ctx) -> const char* { + return impl_.parse(ctx); + } + + template + auto format(E value, FormatContext& ctx) const -> decltype(ctx.out()) { + auto id = detail::identifier_of(value); + if (id.size() != 0) return impl_.format(id, ctx); + // Fall back to the underlying value if there is no matching enumerator. + auto buf = memory_buffer(); + detail::write(appender(buf), underlying(value)); + return impl_.format(string_view(buf.data(), buf.size()), ctx); + } +}; + +#endif // FMT_USE_REFLECTION + +FMT_END_NAMESPACE + +#endif // FMT_REFLECT_H_ diff --git a/support/python/mkdocstrings_handlers/cxx/__init__.py b/support/python/mkdocstrings_handlers/cxx/__init__.py index d83811d8a764..0e2baa3c9532 100644 --- a/support/python/mkdocstrings_handlers/cxx/__init__.py +++ b/support/python/mkdocstrings_handlers/cxx/__init__.py @@ -254,6 +254,7 @@ def __init__( "ostream.h", "printf.h", "ranges.h", + "reflect.h", "std.h", "xchar.h", ] diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a294d9da819b..4f7505dd1b29 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -77,6 +77,41 @@ add_fmt_test(enforce-checks-test) target_compile_definitions(enforce-checks-test PRIVATE -DFMT_ENFORCE_COMPILE_STRING) +# Formatting based on C++26 reflection requires compiler support and, in some +# compilers such as GCC, an extra flag to enable it. +if (NOT MSVC) + include(CheckCXXSourceCompiles) + set(FMT_REFLECT_TEST_CODE + " + #include + enum class [[=fmt::as_identifiers]] color { red }; + static_assert(fmt::is_formattable::value, \"\"); + int main() {} + ") + set(CMAKE_REQUIRED_INCLUDES ${PROJECT_SOURCE_DIR}/include) + set(CMAKE_REQUIRED_FLAGS "-std=c++26") + check_cxx_source_compiles("${FMT_REFLECT_TEST_CODE}" FMT_HAVE_REFLECTION) + if (NOT FMT_HAVE_REFLECTION) + set(CMAKE_REQUIRED_FLAGS "-std=c++26 -freflection") + check_cxx_source_compiles("${FMT_REFLECT_TEST_CODE}" + FMT_HAVE_REFLECTION_FLAG) + endif () + unset(CMAKE_REQUIRED_FLAGS) + unset(CMAKE_REQUIRED_INCLUDES) + + if (FMT_HAVE_REFLECTION OR FMT_HAVE_REFLECTION_FLAG) + add_fmt_test(reflect-test) + set_target_properties( + reflect-test + PROPERTIES CXX_STANDARD 26 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF) + if (FMT_HAVE_REFLECTION_FLAG) + target_compile_options(reflect-test PRIVATE -freflection) + endif () + endif () +endif () + add_executable(perf-sanity perf-sanity.cc) target_link_libraries(perf-sanity fmt::fmt) diff --git a/test/reflect-test.cc b/test/reflect-test.cc new file mode 100644 index 000000000000..46dbaf4f6522 --- /dev/null +++ b/test/reflect-test.cc @@ -0,0 +1,72 @@ +// Formatting library for C++ - reflection tests +// +// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors +// All rights reserved. +// +// For the license information refer to format.h. + +#include "fmt/reflect.h" + +#include + +#include "fmt/ranges.h" +#include "gtest/gtest.h" + +#if !FMT_USE_REFLECTION +TEST(reflect_test, no_reflection) { + fmt::print("Reflection is not supported.\n"); +} +#else + +// clang-format doesn't support annotations yet. +// clang-format off +enum class [[=fmt::as_identifiers]] color { red, green, blue }; +enum class color_without_annotation { red, green, blue }; +enum [[=fmt::as_identifiers]] unscoped_color { unscoped_red, unscoped_green }; +enum class [[=fmt::as_identifiers]] level : unsigned char { low = 1, high = 2 }; +enum class [[=fmt::as_identifiers]] alias { one = 1, uno = 1 }; +enum class [[=fmt::as_identifiers]] empty_enum {}; +// clang-format on + +TEST(reflect_test, format_enum) { + EXPECT_EQ(fmt::format("{}", color::red), "red"); + EXPECT_EQ(fmt::format("{}", color::green), "green"); + EXPECT_EQ(fmt::format("{}", color::blue), "blue"); +} + +TEST(reflect_test, format_unscoped_enum) { + EXPECT_EQ(fmt::format("{}", unscoped_green), "unscoped_green"); +} + +TEST(reflect_test, format_enum_with_underlying_type) { + EXPECT_EQ(fmt::format("{}", level::high), "high"); +} + +TEST(reflect_test, format_enum_alias) { + // The first enumerator with a matching value is used. + EXPECT_EQ(fmt::format("{}", alias::uno), "one"); +} + +TEST(reflect_test, format_unknown_value) { + EXPECT_EQ(fmt::format("{}", static_cast(42)), "42"); + EXPECT_EQ(fmt::format("{}", static_cast(42)), "42"); + EXPECT_EQ(fmt::format("{}", static_cast(0)), "0"); +} + +TEST(reflect_test, format_enum_specs) { + EXPECT_EQ(fmt::format("{:>7}", color::red), " red"); + EXPECT_EQ(fmt::format("{:*^7}", color::red), "**red**"); + EXPECT_EQ(fmt::format("{:.2}", color::green), "gr"); + EXPECT_EQ(fmt::format("{:>4}", static_cast(42)), " 42"); +} + +TEST(reflect_test, format_enum_range) { + auto v = std::vector{color::red, color::blue}; + EXPECT_EQ(fmt::format("{}", v), "[red, blue]"); +} + +TEST(reflect_test, annotation_is_required) { + EXPECT_TRUE(fmt::is_formattable::value); + EXPECT_FALSE(fmt::is_formattable::value); +} +#endif // FMT_USE_REFLECTION