From e07ca22b0fac9ec0aa111d6bc1c27561ea447c91 Mon Sep 17 00:00:00 2001 From: Avi Kivity Date: Thu, 13 Aug 2026 15:06:15 +0300 Subject: [PATCH 1/4] Format enums annotated with fmt::as_identifiers (requires C++26 reflection) 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" Values that don't match any enumerator are represented as their underlying value in decimal before applying string formatting. 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. The header is also part of the fmt module, but, unlike with headers, whether it provides anything is decided when the module is compiled, so the module build detects reflection and enables it if the configured standard allows. Co-Authored-By: Claude Opus 5 (1M context) --- CMakeLists.txt | 33 +++++ doc/api.md | 50 +++++++ include/fmt/enum.h | 123 ++++++++++++++++++ src/fmt.cc | 7 + .../mkdocstrings_handlers/cxx/__init__.py | 1 + test/CMakeLists.txt | 28 ++++ test/enum-test.cc | 87 +++++++++++++ test/module-test.cc | 19 +++ 8 files changed, 348 insertions(+) create mode 100644 include/fmt/enum.h create mode 100644 test/enum-test.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index 5179ce6b3ee0..5b5274a092c0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -275,6 +275,7 @@ foreach ( color.h compile.h core.h + enum.h format.h format-inl.h os.h @@ -408,9 +409,41 @@ function (add_module_library name) target_sources(${name} PRIVATE ${sources}) endfunction () +# Code that compiles only if C++26 reflection, required by fmt/enum.h, is +# available. Also used by test/CMakeLists.txt. +set(FMT_REFLECTION_TEST_CODE + " + #include + enum class [[=fmt::as_identifiers]] color { red }; + static_assert(fmt::is_formattable::value, \"\"); + int main() {} +") + if (FMT_MODULE) + # Unlike with headers, whether fmt/enum.h provides anything is decided when + # the module is compiled, so detect reflection with the configured standard + # and enable it if possible. Reflection needs C++26 and, in some compilers + # such as GCC, an extra flag. + if (NOT MSVC AND CMAKE_CXX_STANDARD GREATER_EQUAL 26) + include(CheckCXXSourceCompiles) + set(CMAKE_REQUIRED_INCLUDES ${PROJECT_SOURCE_DIR}/include) + check_cxx_source_compiles("${FMT_REFLECTION_TEST_CODE}" + FMT_MODULE_HAVE_REFLECTION) + if (NOT FMT_MODULE_HAVE_REFLECTION) + set(CMAKE_REQUIRED_FLAGS -freflection) + check_cxx_source_compiles("${FMT_REFLECTION_TEST_CODE}" + FMT_MODULE_HAVE_REFLECTION_FLAG) + unset(CMAKE_REQUIRED_FLAGS) + endif () + unset(CMAKE_REQUIRED_INCLUDES) + endif () + add_module_library(fmt-module src/fmt.cc USE_CMAKE_MODULES ${FMT_USE_CMAKE_MODULES}) + if (FMT_MODULE_HAVE_REFLECTION_FLAG) + # PUBLIC because importers need the flag to use annotations. + target_compile_options(fmt-module PUBLIC -freflection) + endif () setup_target(fmt-module PUBLIC) endif () diff --git a/doc/api.md b/doc/api.md index 7ea03e260656..fb39083ec7c5 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/enum.h`](#enum-api): formatting of annotated enums - [`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,55 @@ fmt::print("{}", +s.bit); This is a known limitation of "perfect" forwarding in C++. + +## Enum Formatting + +`fmt/enum.h` provides formatting of enums annotated with +`fmt::as_identifiers`. Such an enum 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 represented +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. +scoped enums require `format_as` or a `formatter` specialization, see +[Formatting User-Defined Types](#udt). + +Identifiers are retrieved with C++26 reflection ([P2996]( +https://wg21.link/p2996)) and the annotation with [P3394]( +https://wg21.link/p3394), so this 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, in which +case `fmt/enum.h` is empty. + +When {fmt} is built as a module, reflection support is detected when the module +itself is compiled, so this API is only available to importers if the module was +built with reflection enabled. An importing translation unit may also have to +include `` itself: some compilers, such as GCC 16, fail to look up +implementation details of `std::define_static_string` when instantiating the +formatter otherwise. + ## Compile-Time Support diff --git a/include/fmt/enum.h b/include/fmt/enum.h new file mode 100644 index 000000000000..58ec11405471 --- /dev/null +++ b/include/fmt/enum.h @@ -0,0 +1,123 @@ +// Formatting library for C++ - formatting of enums +// +// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors +// All rights reserved. +// +// For the license information refer to format.h. + +#ifndef FMT_ENUM_H_ +#define FMT_ENUM_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) +# 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. +FMT_EXPORT 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 represented as its underlying + * value in decimal before applying string formatting. + */ +FMT_EXPORT 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); + // identifier_of returns a view of a string with static storage duration. + ids[i++] = {std::meta::extract(e), string_view(id.data(), 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. + // Unary plus applies integral promotion so that `char` and `bool` + // underlying types are written in decimal instead of as a character or + // "true"/"false". + 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_ENUM_H_ diff --git a/src/fmt.cc b/src/fmt.cc index 6d1d73b59f78..7f51c356c95d 100644 --- a/src/fmt.cc +++ b/src/fmt.cc @@ -66,6 +66,12 @@ module; #include #include +// fmt/enum.h uses C++26 reflection if it is available. +#if defined(__cpp_impl_reflection) && __has_include() +# include +# include +#endif + #if __has_include() # include #endif @@ -128,6 +134,7 @@ extern "C++" { #include "fmt/chrono.h" #include "fmt/color.h" #include "fmt/compile.h" +#include "fmt/enum.h" #include "fmt/format.h" #if FMT_OS # include "fmt/os.h" diff --git a/support/python/mkdocstrings_handlers/cxx/__init__.py b/support/python/mkdocstrings_handlers/cxx/__init__.py index d83811d8a764..a55e85b2622c 100644 --- a/support/python/mkdocstrings_handlers/cxx/__init__.py +++ b/support/python/mkdocstrings_handlers/cxx/__init__.py @@ -249,6 +249,7 @@ def __init__( "chrono.h", "color.h", "compile.h", + "enum.h", "format.h", "os.h", "ostream.h", diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a294d9da819b..a4672b4a47ac 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -77,6 +77,34 @@ add_fmt_test(enforce-checks-test) target_compile_definitions(enforce-checks-test PRIVATE -DFMT_ENFORCE_COMPILE_STRING) +# Enum formatting requires C++26 reflection, which needs compiler support and, +# in some compilers such as GCC, an extra flag to enable it. +if (NOT MSVC) + include(CheckCXXSourceCompiles) + set(CMAKE_REQUIRED_INCLUDES ${PROJECT_SOURCE_DIR}/include) + set(CMAKE_REQUIRED_FLAGS "-std=c++26") + check_cxx_source_compiles("${FMT_REFLECTION_TEST_CODE}" FMT_HAVE_REFLECTION) + if (NOT FMT_HAVE_REFLECTION) + set(CMAKE_REQUIRED_FLAGS "-std=c++26 -freflection") + check_cxx_source_compiles("${FMT_REFLECTION_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(enum-test) + set_target_properties( + enum-test + PROPERTIES CXX_STANDARD 26 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF) + if (FMT_HAVE_REFLECTION_FLAG) + target_compile_options(enum-test PRIVATE -freflection) + endif () + endif () +endif () + add_executable(perf-sanity perf-sanity.cc) target_link_libraries(perf-sanity fmt::fmt) diff --git a/test/enum-test.cc b/test/enum-test.cc new file mode 100644 index 000000000000..7b886ff07c18 --- /dev/null +++ b/test/enum-test.cc @@ -0,0 +1,87 @@ +// Formatting library for C++ - enum formatting tests +// +// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors +// All rights reserved. +// +// For the license information refer to format.h. + +#include "fmt/enum.h" + +#include + +#include "fmt/ranges.h" +#include "gtest/gtest.h" + +#if !FMT_USE_REFLECTION +TEST(enum_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]] byte_enum : char { one = 1 }; +enum class [[=fmt::as_identifiers]] signed_byte_enum : signed char { minus_one = -1 }; +enum class [[=fmt::as_identifiers]] bool_enum : bool { off = false }; +enum class [[=fmt::as_identifiers]] alias { one = 1, uno = 1 }; +enum class [[=fmt::as_identifiers]] empty_enum {}; +// clang-format on + +TEST(enum_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(enum_test, format_unscoped_enum) { + EXPECT_EQ(fmt::format("{}", unscoped_green), "unscoped_green"); +} + +TEST(enum_test, format_enum_with_underlying_type) { + EXPECT_EQ(fmt::format("{}", level::high), "high"); + EXPECT_EQ(fmt::format("{}", byte_enum::one), "one"); + EXPECT_EQ(fmt::format("{}", signed_byte_enum::minus_one), "minus_one"); + EXPECT_EQ(fmt::format("{}", bool_enum::off), "off"); +} + +TEST(enum_test, format_enum_alias) { + // The first enumerator with a matching value is used. + EXPECT_EQ(fmt::format("{}", alias::uno), "one"); +} + +TEST(enum_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(enum_test, format_unknown_char_value) { + // A char underlying type is written in decimal, not as a character. + EXPECT_EQ(fmt::format("{}", static_cast(65)), "65"); + EXPECT_EQ(fmt::format("{}", static_cast(-65)), "-65"); + EXPECT_EQ(fmt::format("{}", static_cast(65)), "65"); + // A bool underlying type is written as 0 or 1, not as "true"/"false". + EXPECT_EQ(fmt::format("{}", static_cast(true)), "1"); +} + +TEST(enum_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(enum_test, format_enum_range) { + auto v = std::vector{color::red, color::blue}; + EXPECT_EQ(fmt::format("{}", v), "[red, blue]"); +} + +TEST(enum_test, annotation_is_required) { + EXPECT_TRUE(fmt::is_formattable::value); + EXPECT_FALSE(fmt::is_formattable::value); +} +#endif // FMT_USE_REFLECTION diff --git a/test/module-test.cc b/test/module-test.cc index a7b67a4a3623..e266a8797e98 100644 --- a/test/module-test.cc +++ b/test/module-test.cc @@ -15,6 +15,10 @@ #include #include #include +#include +#if defined(__cpp_impl_reflection) && __has_include() +# include // to instantiate the formatter for annotated enums +#endif #include #if (__has_include() || defined(__APPLE__) || \ @@ -366,3 +370,18 @@ TEST(module_test, compile_format_string) { EXPECT_EQ(L" 42", fmt::format(L"{arg:>3}"_cf, L"arg"_a = L"42")); #endif } + +// Enum formatting is only exported if the module was built with C++26 +// reflection enabled. +#if defined(__cpp_impl_reflection) && defined(__cpp_lib_reflection) && \ + defined(__cpp_lib_define_static) +// clang-format doesn't support annotations yet. +// clang-format off +enum class [[=fmt::as_identifiers]] color { red, green, blue }; +// clang-format on + +TEST(module_test, format_enum) { + EXPECT_EQ("green", fmt::format("{}", color::green)); + EXPECT_EQ(" red", fmt::format("{:>5}", color::red)); +} +#endif From 2e45b128b606c8c3951a37795353febcb878176a Mon Sep 17 00:00:00 2001 From: Avi Kivity Date: Sun, 23 Aug 2026 14:32:08 +0300 Subject: [PATCH 2/4] Look up enum identifiers by index when the values are dense The formatter for enums annotated with fmt::as_identifiers did a linear search over all enumerators. Build a table indexed by the distance from the smallest enumerator value instead, with empty string views in the holes, which reduces the lookup to a bounds check and one load. The table is only used if at least 70% of its elements are identifiers, limiting its size to 10/7 of the number of enumerators. Sparser enums keep using the linear search. Distances are computed in uint64_t so that enums with negative values and values spanning the whole range of the underlying type are handled without overflow. Co-Authored-By: Claude Opus 5 (1M context) --- include/fmt/enum.h | 80 +++++++++++++++++++++++++++++++++++++++++----- test/enum-test.cc | 79 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 8 deletions(-) diff --git a/include/fmt/enum.h b/include/fmt/enum.h index 58ec11405471..841f8daef3af 100644 --- a/include/fmt/enum.h +++ b/include/fmt/enum.h @@ -61,19 +61,75 @@ consteval auto use_identifiers() -> bool { } } +// Returns the identifier of e. identifier_of returns a view of a string with +// static storage duration so it doesn't need to be copied. +inline consteval auto identifier(std::meta::info e) -> string_view { + auto id = std::meta::identifier_of(e); + return string_view(id.data(), id.size()); +} + +// 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. +template constexpr auto to_uint64(E value) -> uint64_t { + return static_cast(static_cast>(value)); +} + template consteval auto count_enumerators() -> size_t { return std::meta::enumerators_of(^^E).size(); } +// Returns the smallest enumerator value of E or 0 if E has no enumerators. +template consteval auto min_enumerator() -> E { + auto enumerators = std::meta::enumerators_of(^^E); + if (enumerators.empty()) return E(); + auto min = std::meta::extract(enumerators[0]); + for (std::meta::info e : enumerators) { + auto value = std::meta::extract(e); + if (value < min) min = value; + } + return min; +} + +// Returns the size of a table that maps the distance from the smallest +// enumerator value of E to an identifier, or 0 if the values are too sparse +// for such a table to be worthwhile. +template consteval auto identifier_table_size() -> size_t { + auto enumerators = std::meta::enumerators_of(^^E); + if (enumerators.empty()) return 0; + auto min = to_uint64(min_enumerator()); + auto span = uint64_t(); + for (std::meta::info e : enumerators) + span = max_of(span, to_uint64(std::meta::extract(e)) - min); + // Require the density of at least 70% to limit the size of the table. + // Comparing with the limit instead of adding one to span avoids overflow. + auto limit = uint64_t(enumerators.size()) * 10 / 7; + return span < limit ? static_cast(span) + 1 : 0; +} + +template ()> +consteval auto make_identifier_table() -> std::array { + auto ids = std::array(); + auto min = to_uint64(min_enumerator()); + for (std::meta::info e : std::meta::enumerators_of(^^E)) { + auto i = static_cast(to_uint64(std::meta::extract(e)) - min); + // Keep the identifier of the first enumerator with this value. + if (ids[i].size() == 0) ids[i] = identifier(e); + } + return ids; +} + +// Identifiers of enumerators of E indexed by the distance from the smallest +// enumerator value with empty string views in the holes. +template +inline constexpr auto identifier_table = make_identifier_table(); + 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); - // identifier_of returns a view of a string with static storage duration. - ids[i++] = {std::meta::extract(e), string_view(id.data(), id.size())}; - } + for (std::meta::info e : std::meta::enumerators_of(^^E)) + ids[i++] = {std::meta::extract(e), identifier(e)}; return ids; } @@ -83,10 +139,18 @@ 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; + constexpr size_t table_size = identifier_table_size(); + if constexpr (table_size != 0) { + // Values outside of the table wrap around and are rejected by the check. + auto i = to_uint64(value) - to_uint64(min_enumerator()); + return i < table_size ? identifier_table[static_cast(i)] + : string_view(); + } else { + for (const auto& id : identifiers) { + if (id.first == value) return id.second; + } + return {}; } - return {}; } } // namespace detail diff --git a/test/enum-test.cc b/test/enum-test.cc index 7b886ff07c18..662399e3e5c3 100644 --- a/test/enum-test.cc +++ b/test/enum-test.cc @@ -7,6 +7,7 @@ #include "fmt/enum.h" +#include #include #include "fmt/ranges.h" @@ -29,6 +30,30 @@ enum class [[=fmt::as_identifiers]] signed_byte_enum : signed char { minus_one = enum class [[=fmt::as_identifiers]] bool_enum : bool { off = false }; enum class [[=fmt::as_identifiers]] alias { one = 1, uno = 1 }; enum class [[=fmt::as_identifiers]] empty_enum {}; + +// Dense values: formatted via a lookup table. +enum class [[=fmt::as_identifiers]] dense { d0, d1, d2, d3, d4 }; +// 3 holes out of 10: the sparsest case that still uses a lookup table. +enum class [[=fmt::as_identifiers]] holey { + h0, h1, h2, h3, h4, h5, h6 = 9 +}; +// 4 holes out of 11: just too sparse, formatted via a linear search. +enum class [[=fmt::as_identifiers]] sparse { + s0, s1, s2, s3, s4, s5, s6 = 10 +}; +// Values spanning both signs and the extremes of the underlying type. +enum class [[=fmt::as_identifiers]] signed_enum { + minus_two = -2, + minus_one = -1, + one = 1 +}; +enum class [[=fmt::as_identifiers]] extremes : int { + lowest = INT_MIN, + highest = INT_MAX +}; +enum class [[=fmt::as_identifiers]] big : unsigned long long { + huge = ULLONG_MAX +}; // clang-format on TEST(enum_test, format_enum) { @@ -48,6 +73,60 @@ TEST(enum_test, format_enum_with_underlying_type) { EXPECT_EQ(fmt::format("{}", bool_enum::off), "off"); } +TEST(enum_test, format_dense_enum) { + // A dense enum is formatted using a lookup table. + static_assert(fmt::detail::identifier_table_size() == 5); + EXPECT_EQ(fmt::format("{}", dense::d0), "d0"); + EXPECT_EQ(fmt::format("{}", dense::d4), "d4"); + EXPECT_EQ(fmt::format("{}", static_cast(5)), "5"); +} + +TEST(enum_test, format_holey_enum) { + // Up to 30% of holes are still formatted using a lookup table. + static_assert(fmt::detail::identifier_table_size() == 10); + EXPECT_EQ(fmt::format("{}", holey::h0), "h0"); + EXPECT_EQ(fmt::format("{}", holey::h5), "h5"); + EXPECT_EQ(fmt::format("{}", holey::h6), "h6"); + // Values in the holes and outside of the table fall back to the number. + EXPECT_EQ(fmt::format("{}", static_cast(6)), "6"); + EXPECT_EQ(fmt::format("{}", static_cast(8)), "8"); + EXPECT_EQ(fmt::format("{}", static_cast(10)), "10"); + EXPECT_EQ(fmt::format("{}", static_cast(-1)), "-1"); +} + +TEST(enum_test, format_sparse_enum) { + // One more hole than holey, which is too many for a lookup table. + static_assert(fmt::detail::identifier_table_size() == 0); + EXPECT_EQ(fmt::format("{}", sparse::s0), "s0"); + EXPECT_EQ(fmt::format("{}", sparse::s5), "s5"); + EXPECT_EQ(fmt::format("{}", sparse::s6), "s6"); + EXPECT_EQ(fmt::format("{}", static_cast(6)), "6"); + EXPECT_EQ(fmt::format("{}", static_cast(11)), "11"); +} + +TEST(enum_test, format_enum_with_negative_values) { + static_assert(fmt::detail::identifier_table_size() == 4); + EXPECT_EQ(fmt::format("{}", signed_enum::minus_two), "minus_two"); + EXPECT_EQ(fmt::format("{}", signed_enum::minus_one), "minus_one"); + EXPECT_EQ(fmt::format("{}", signed_enum::one), "one"); + // A hole and values below and above the range of the table. + EXPECT_EQ(fmt::format("{}", static_cast(0)), "0"); + EXPECT_EQ(fmt::format("{}", static_cast(-3)), "-3"); + EXPECT_EQ(fmt::format("{}", static_cast(2)), "2"); +} + +TEST(enum_test, format_enum_with_extreme_values) { + // The span of the values overflows the underlying type, so no table is used. + static_assert(fmt::detail::identifier_table_size() == 0); + EXPECT_EQ(fmt::format("{}", extremes::lowest), "lowest"); + EXPECT_EQ(fmt::format("{}", extremes::highest), "highest"); + EXPECT_EQ(fmt::format("{}", static_cast(0)), "0"); + + static_assert(fmt::detail::identifier_table_size() == 1); + EXPECT_EQ(fmt::format("{}", big::huge), "huge"); + EXPECT_EQ(fmt::format("{}", static_cast(0)), "0"); +} + TEST(enum_test, format_enum_alias) { // The first enumerator with a matching value is used. EXPECT_EQ(fmt::format("{}", alias::uno), "one"); From a70d6983c9413cb45c8d255c0e294139abfce8c3 Mon Sep 17 00:00:00 2001 From: Avi Kivity Date: Fri, 21 Aug 2026 16:58:35 +0300 Subject: [PATCH 3/4] Suppress -Wsfinae-incomplete in format-test on GCC 16 GCC 16 warns when a type is completed after it failed to be complete in a SFINAE context. format-test does this deliberately to check that formatting of incomplete types works, so the warning is a false positive there and breaks the build with -Werror. --- test/CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a4672b4a47ac..6eadfbb09946 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -50,6 +50,11 @@ add_fmt_test(format-test mock-allocator.h) if (MSVC) target_compile_options(format-test PRIVATE /bigobj) endif () +if (CMAKE_COMPILER_IS_GNUCXX AND NOT CMAKE_CXX_COMPILER_VERSION VERSION_LESS 16) + # format-test completes a type after it failed to be complete in a SFINAE + # context, which is exactly what it is testing. + target_compile_options(format-test PRIVATE -Wno-sfinae-incomplete) +endif () if (NOT (MSVC AND BUILD_SHARED_LIBS)) add_fmt_test(format-impl-test HEADER_ONLY header-only-test.cc) endif () From 2f48cd05dab058cf2fa94fc24f00c83b87d3de9d Mon Sep 17 00:00:00 2001 From: Avi Kivity Date: Fri, 21 Aug 2026 16:59:44 +0300 Subject: [PATCH 4/4] Test GCC 16 on CI GCC 16 is the first compiler with C++26 reflection support, which is needed by fmt/enum.h, so add a job that builds with it in C++26 mode. It comes from the ubuntu-toolchain-r/test PPA since Ubuntu 24.04 only ships GCC 14. Also report when reflection is not detected to make it visible that enum-test was skipped. --- .github/workflows/linux.yml | 10 ++++++++++ test/CMakeLists.txt | 2 ++ 2 files changed, 12 insertions(+) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index b3e511f86f60..c24bb8a48aeb 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -46,6 +46,11 @@ jobs: std: 23 os: ubuntu-24.04 gen: Ninja + - cxx: g++-16 + build_type: Debug + std: 26 + os: ubuntu-24.04 + install: sudo apt install g++-16 - cxx: clang++-3.6 - cxx: clang++-11 build_type: Debug @@ -160,6 +165,11 @@ jobs: | sudo tee /etc/apt/sources.list.d/llvm.list if: ${{ matrix.cxx == 'clang++-20' }} + - name: Install GCC 16 + run: | + sudo add-apt-repository --no-update --yes ppa:ubuntu-toolchain-r/test + if: ${{ matrix.cxx == 'g++-16' }} + - name: Add Ubuntu mirrors run: | # GitHub Actions caching proxy is at times unreliable diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6eadfbb09946..ce1c1f15ad60 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -107,6 +107,8 @@ if (NOT MSVC) if (FMT_HAVE_REFLECTION_FLAG) target_compile_options(enum-test PRIVATE -freflection) endif () + else () + message(STATUS "Reflection is not supported, skipping enum-test.") endif () endif ()