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