Skip to content

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

Merged
vitaut merged 4 commits into
fmtlib:mainfrom
avikivity:enum-autoformat
Aug 23, 2026
Merged

Format enums annotated with fmt::as_identifiers (requires C++26 reflection)#4885
vitaut merged 4 commits into
fmtlib:mainfrom
avikivity:enum-autoformat

Conversation

@avikivity

@avikivity avikivity commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

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.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com

@avikivity

Copy link
Copy Markdown
Contributor Author

v2: apply cmake-format

@avikivity

Copy link
Copy Markdown
Contributor Author

@vitaut please review

@vitaut vitaut left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great, very cool use of C++26 annotations for enum formatting. Thanks for putting this together!

One thing I noticed: it looks like the reflection tests are currently skipped everywhere because none of the CI compilers support reflection. It would be good to have at least one GCC 16 + -freflection job that actually builds and runs this code.

Comment thread doc/api.md Outdated
Comment thread include/fmt/reflect.h Outdated
@avikivity avikivity changed the title Add fmt/reflect.h and format enums annotated with fmt::as_identifiers Format enums annotated with fmt::as_identifiers (requires C++26 reflection) Aug 21, 2026
@avikivity

Copy link
Copy Markdown
Contributor Author

Update:

  • renamed reflect.h to enum.h and made it more about enums rather than reflection
  • improved wording around how enum values that don't match any identifier are formatted
  • added gcc 16 to CI

@avikivity
avikivity requested a review from vitaut August 21, 2026 14:10
@avikivity

Copy link
Copy Markdown
Contributor Author

This looks great, very cool use of C++26 annotations for enum formatting. Thanks for putting this together!

One thing I noticed: it looks like the reflection tests are currently skipped everywhere because none of the CI compilers support reflection. It would be good to have at least one GCC 16 + -freflection job that actually builds and runs this code.

Done.

@avikivity

Copy link
Copy Markdown
Contributor Author

Update: support C++20 modules too

@vitaut vitaut left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly looks good but I think unknown values of enum : char will be formatted as char rather than decimal and also defined_static_string seems unnecessary.

Comment thread include/fmt/enum.h Outdated
* 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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this should be singular as_identifier because the result is a single identifier?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read it as "format this enum type as its identifiers" rather than "format this enum value as the corresponding identifier", since the type is annotated.

Comment thread include/fmt/enum.h Outdated
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())};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think define_static_string is necessary. identifier_of should already provide the required lifetime guarantee.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right.

…ction)

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>
@avikivity

Copy link
Copy Markdown
Contributor Author

Mostly looks good but I think unknown values of enum : char will be formatted as char rather than decimal

Good catch, fixed.

and also defined_static_string seems unnecessary.

Yes

avikivity and others added 3 commits August 23, 2026 14:33
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>
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.
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.
@avikivity

avikivity commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

Update:

  • removed unneeded define_static_string
  • fixed formatting of enum class with underlying type of char when no identifier is found
  • optimized the case where the enum is contiguous or near-contiguous to use array indexing rather than linear search (new commit)

Unchanged:

  • still using fmt::as_identifiers, happy to adjust if needed

@avikivity
avikivity requested a review from vitaut August 23, 2026 12:13
@vitaut
vitaut merged commit e27cc20 into fmtlib:main Aug 23, 2026
47 checks passed
@vitaut

vitaut commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Merged, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants