Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down
41 changes: 41 additions & 0 deletions doc/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -556,6 +557,46 @@ fmt::print("{}", +s.bit);

This is a known limitation of "perfect" forwarding in C++.

<a id="reflect-api"></a>
## 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 <fmt/reflect.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 formatted as
the corresponding value of the underlying type:

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

<a id="compile-api"></a>
## Compile-Time Support

Expand Down
121 changes: 121 additions & 0 deletions include/fmt/reflect.h
Original file line number Diff line number Diff line change
@@ -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(<version>)
# include <version>
#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 <array>
# include <meta>
# include <utility> // 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 <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();
}
}

template <typename E> consteval auto count_enumerators() -> size_t {
return std::meta::enumerators_of(^^E).size();
}

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)) {
auto id = std::meta::identifier_of(e);
ids[i++] = {std::meta::extract<E>(e),
string_view(std::define_static_string(id), id.size())};
}
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 {
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.
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_REFLECT_H_
1 change: 1 addition & 0 deletions support/python/mkdocstrings_handlers/cxx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ def __init__(
"ostream.h",
"printf.h",
"ranges.h",
"reflect.h",
"std.h",
"xchar.h",
]
Expand Down
35 changes: 35 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 <fmt/reflect.h>
enum class [[=fmt::as_identifiers]] color { red };
static_assert(fmt::is_formattable<color>::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)

Expand Down
72 changes: 72 additions & 0 deletions test/reflect-test.cc
Original file line number Diff line number Diff line change
@@ -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 <vector>

#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<color>(42)), "42");
EXPECT_EQ(fmt::format("{}", static_cast<level>(42)), "42");
EXPECT_EQ(fmt::format("{}", static_cast<empty_enum>(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<color>(42)), " 42");
}

TEST(reflect_test, format_enum_range) {
auto v = std::vector<color>{color::red, color::blue};
EXPECT_EQ(fmt::format("{}", v), "[red, blue]");
}

TEST(reflect_test, annotation_is_required) {
EXPECT_TRUE(fmt::is_formattable<color>::value);
EXPECT_FALSE(fmt::is_formattable<color_without_annotation>::value);
}
#endif // FMT_USE_REFLECTION
Loading