Skip to content
Merged
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
10 changes: 10 additions & 0 deletions .github/workflows/linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ foreach (
color.h
compile.h
core.h
enum.h
format.h
format-inl.h
os.h
Expand Down Expand Up @@ -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 <fmt/enum.h>
enum class [[=fmt::as_identifiers]] color { red };
static_assert(fmt::is_formattable<color>::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 ()

Expand Down
50 changes: 50 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/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
Expand Down Expand Up @@ -556,6 +557,55 @@ fmt::print("{}", +s.bit);

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>
## Compile-Time Support

Expand Down
187 changes: 187 additions & 0 deletions include/fmt/enum.h
Original file line number Diff line number Diff line change
@@ -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_
7 changes: 7 additions & 0 deletions src/fmt.cc
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ module;
#include <climits>
#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>)
# include <cxxabi.h>
#endif
Expand Down Expand Up @@ -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"
Expand Down
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 @@ -249,6 +249,7 @@ def __init__(
"chrono.h",
"color.h",
"compile.h",
"enum.h",
"format.h",
"os.h",
"ostream.h",
Expand Down
Loading
Loading