Format enums annotated with fmt::as_identifiers (requires C++26 reflection) (#4885)

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* 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 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.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Avi Kivity 2026-08-23 19:31:28 +03:00 committed by GitHub
parent 488c3b9628
commit e27cc20bd9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 508 additions and 0 deletions

View File

@ -46,6 +46,11 @@ jobs:
std: 23 std: 23
os: ubuntu-24.04 os: ubuntu-24.04
gen: Ninja 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++-3.6
- cxx: clang++-11 - cxx: clang++-11
build_type: Debug build_type: Debug
@ -160,6 +165,11 @@ jobs:
| sudo tee /etc/apt/sources.list.d/llvm.list | sudo tee /etc/apt/sources.list.d/llvm.list
if: ${{ matrix.cxx == 'clang++-20' }} 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 - name: Add Ubuntu mirrors
run: | run: |
# GitHub Actions caching proxy is at times unreliable # GitHub Actions caching proxy is at times unreliable

View File

@ -275,6 +275,7 @@ foreach (
color.h color.h
compile.h compile.h
core.h core.h
enum.h
format.h format.h
format-inl.h format-inl.h
os.h os.h
@ -408,9 +409,41 @@ function (add_module_library name)
target_sources(${name} PRIVATE ${sources}) target_sources(${name} PRIVATE ${sources})
endfunction () 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 <fmt/enum.h>
enum class [[=fmt::as_identifiers]] color { red };
static_assert(fmt::is_formattable<color>::value, \"\");
int main() {}
")
if (FMT_MODULE) 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 add_module_library(fmt-module src/fmt.cc USE_CMAKE_MODULES
${FMT_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) setup_target(fmt-module PUBLIC)
endif () endif ()

View File

@ -9,6 +9,7 @@ The {fmt} library API consists of the following components:
- [`fmt/ranges.h`](#ranges-api): formatting of ranges and tuples - [`fmt/ranges.h`](#ranges-api): formatting of ranges and tuples
- [`fmt/chrono.h`](#chrono-api): date and time formatting - [`fmt/chrono.h`](#chrono-api): date and time formatting
- [`fmt/std.h`](#std-api): formatters for standard library types - [`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/compile.h`](#compile-api): format string compilation
- [`fmt/color.h`](#color-api): terminal colors and text styles - [`fmt/color.h`](#color-api): terminal colors and text styles
- [`fmt/os.h`](#os-api): system APIs - [`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++. This is a known limitation of "perfect" forwarding in C++.
<a id="enum-api"></a>
## 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 <fmt/enum.h>
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<color>(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 `<meta>` itself: some compilers, such as GCC 16, fail to look up
implementation details of `std::define_static_string` when instantiating the
formatter otherwise.
<a id="compile-api"></a> <a id="compile-api"></a>
## Compile-Time Support ## Compile-Time Support

187
include/fmt/enum.h Normal file
View File

@ -0,0 +1,187 @@
// 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(<version>)
# include <version>
#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 <array>
# include <meta>
# include <utility> // 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 <typename T, typename U = remove_cvref_t<T>>
consteval auto use_identifiers() -> bool {
if constexpr (!std::is_enum<U>::value) {
return false;
} else {
return !std::meta::annotations_of_with_type(^^U, ^^as_identifiers_t)
.empty();
}
}
// 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 <typename E> constexpr auto to_uint64(E value) -> uint64_t {
return static_cast<uint64_t>(static_cast<underlying_t<E>>(value));
}
template <typename E> 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 <typename E> consteval auto min_enumerator() -> E {
auto enumerators = std::meta::enumerators_of(^^E);
if (enumerators.empty()) return E();
auto min = std::meta::extract<E>(enumerators[0]);
for (std::meta::info e : enumerators) {
auto value = std::meta::extract<E>(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 <typename E> 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<E>());
auto span = uint64_t();
for (std::meta::info e : enumerators)
span = max_of(span, to_uint64(std::meta::extract<E>(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<size_t>(span) + 1 : 0;
}
template <typename E, size_t N = identifier_table_size<E>()>
consteval auto make_identifier_table() -> std::array<string_view, N> {
auto ids = std::array<string_view, N>();
auto min = to_uint64(min_enumerator<E>());
for (std::meta::info e : std::meta::enumerators_of(^^E)) {
auto i = static_cast<size_t>(to_uint64(std::meta::extract<E>(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 <typename E>
inline constexpr auto identifier_table = make_identifier_table<E>();
template <typename E, size_t N = count_enumerators<E>()>
consteval auto make_identifiers() -> std::array<std::pair<E, string_view>, N> {
auto ids = std::array<std::pair<E, string_view>, N>();
auto i = size_t();
for (std::meta::info e : std::meta::enumerators_of(^^E))
ids[i++] = {std::meta::extract<E>(e), identifier(e)};
return ids;
}
// Identifiers of enumerators of E in the order of declaration.
template <typename E> inline constexpr auto identifiers = make_identifiers<E>();
// Returns the identifier of the first enumerator of E equal to value or an
// empty string view if there is no such enumerator.
template <typename E> constexpr auto identifier_of(E value) -> string_view {
constexpr size_t table_size = identifier_table_size<E>();
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<E>());
return i < table_size ? identifier_table<E>[static_cast<size_t>(i)]
: string_view();
} else {
for (const auto& id : identifiers<E>) {
if (id.first == value) return id.second;
}
return {};
}
}
} // namespace detail
// A formatter for enums annotated with fmt::as_identifiers.
template <typename E>
struct formatter<E, char, enable_if_t<detail::use_identifiers<E>()>> {
private:
formatter<string_view> impl_;
public:
FMT_CONSTEXPR auto parse(parse_context<char>& ctx) -> const char* {
return impl_.parse(ctx);
}
template <typename FormatContext>
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<char>(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_

View File

@ -66,6 +66,12 @@ module;
#include <climits> #include <climits>
#include <version> #include <version>
// fmt/enum.h uses C++26 reflection if it is available.
#if defined(__cpp_impl_reflection) && __has_include(<meta>)
# include <array>
# include <meta>
#endif
#if __has_include(<cxxabi.h>) #if __has_include(<cxxabi.h>)
# include <cxxabi.h> # include <cxxabi.h>
#endif #endif
@ -128,6 +134,7 @@ extern "C++" {
#include "fmt/chrono.h" #include "fmt/chrono.h"
#include "fmt/color.h" #include "fmt/color.h"
#include "fmt/compile.h" #include "fmt/compile.h"
#include "fmt/enum.h"
#include "fmt/format.h" #include "fmt/format.h"
#if FMT_OS #if FMT_OS
# include "fmt/os.h" # include "fmt/os.h"

View File

@ -249,6 +249,7 @@ class CxxHandler(BaseHandler):
"chrono.h", "chrono.h",
"color.h", "color.h",
"compile.h", "compile.h",
"enum.h",
"format.h", "format.h",
"os.h", "os.h",
"ostream.h", "ostream.h",

View File

@ -50,6 +50,11 @@ add_fmt_test(format-test mock-allocator.h)
if (MSVC) if (MSVC)
target_compile_options(format-test PRIVATE /bigobj) target_compile_options(format-test PRIVATE /bigobj)
endif () 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)) if (NOT (MSVC AND BUILD_SHARED_LIBS))
add_fmt_test(format-impl-test HEADER_ONLY header-only-test.cc) add_fmt_test(format-impl-test HEADER_ONLY header-only-test.cc)
endif () endif ()
@ -77,6 +82,36 @@ add_fmt_test(enforce-checks-test)
target_compile_definitions(enforce-checks-test target_compile_definitions(enforce-checks-test
PRIVATE -DFMT_ENFORCE_COMPILE_STRING) 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 ()
else ()
message(STATUS "Reflection is not supported, skipping enum-test.")
endif ()
endif ()
add_executable(perf-sanity perf-sanity.cc) add_executable(perf-sanity perf-sanity.cc)
target_link_libraries(perf-sanity fmt::fmt) target_link_libraries(perf-sanity fmt::fmt)

166
test/enum-test.cc Normal file
View File

@ -0,0 +1,166 @@
// 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 <climits>
#include <vector>
#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 {};
// 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) {
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_dense_enum) {
// A dense enum is formatted using a lookup table.
static_assert(fmt::detail::identifier_table_size<dense>() == 5);
EXPECT_EQ(fmt::format("{}", dense::d0), "d0");
EXPECT_EQ(fmt::format("{}", dense::d4), "d4");
EXPECT_EQ(fmt::format("{}", static_cast<dense>(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<holey>() == 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<holey>(6)), "6");
EXPECT_EQ(fmt::format("{}", static_cast<holey>(8)), "8");
EXPECT_EQ(fmt::format("{}", static_cast<holey>(10)), "10");
EXPECT_EQ(fmt::format("{}", static_cast<holey>(-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<sparse>() == 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<sparse>(6)), "6");
EXPECT_EQ(fmt::format("{}", static_cast<sparse>(11)), "11");
}
TEST(enum_test, format_enum_with_negative_values) {
static_assert(fmt::detail::identifier_table_size<signed_enum>() == 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<signed_enum>(0)), "0");
EXPECT_EQ(fmt::format("{}", static_cast<signed_enum>(-3)), "-3");
EXPECT_EQ(fmt::format("{}", static_cast<signed_enum>(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<extremes>() == 0);
EXPECT_EQ(fmt::format("{}", extremes::lowest), "lowest");
EXPECT_EQ(fmt::format("{}", extremes::highest), "highest");
EXPECT_EQ(fmt::format("{}", static_cast<extremes>(0)), "0");
static_assert(fmt::detail::identifier_table_size<big>() == 1);
EXPECT_EQ(fmt::format("{}", big::huge), "huge");
EXPECT_EQ(fmt::format("{}", static_cast<big>(0)), "0");
}
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<color>(42)), "42");
EXPECT_EQ(fmt::format("{}", static_cast<level>(42)), "42");
EXPECT_EQ(fmt::format("{}", static_cast<empty_enum>(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<byte_enum>(65)), "65");
EXPECT_EQ(fmt::format("{}", static_cast<signed_byte_enum>(-65)), "-65");
EXPECT_EQ(fmt::format("{}", static_cast<level>(65)), "65");
// A bool underlying type is written as 0 or 1, not as "true"/"false".
EXPECT_EQ(fmt::format("{}", static_cast<bool_enum>(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<color>(42)), " 42");
}
TEST(enum_test, format_enum_range) {
auto v = std::vector<color>{color::red, color::blue};
EXPECT_EQ(fmt::format("{}", v), "[red, blue]");
}
TEST(enum_test, annotation_is_required) {
EXPECT_TRUE(fmt::is_formattable<color>::value);
EXPECT_FALSE(fmt::is_formattable<color_without_annotation>::value);
}
#endif // FMT_USE_REFLECTION

View File

@ -15,6 +15,10 @@
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <system_error> #include <system_error>
#include <version>
#if defined(__cpp_impl_reflection) && __has_include(<meta>)
# include <meta> // to instantiate the formatter for annotated enums
#endif
#include <gmock/gmock.h> #include <gmock/gmock.h>
#if (__has_include(<fcntl.h>) || defined(__APPLE__) || \ #if (__has_include(<fcntl.h>) || 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")); EXPECT_EQ(L" 42", fmt::format(L"{arg:>3}"_cf, L"arg"_a = L"42"));
#endif #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