register classes by reflection in C++26 - #94
Open
jll63 wants to merge 17 commits into
Open
Conversation
|
An automated preview of the documentation is available at https://94.openmethod.prtest3.cppalliance.org/libs/openmethod/doc/html/index.html If more commits are pushed to the pull request, the docs will rebuild at the same URL. 2026-08-31 23:27:18 UTC |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #94 +/- ##
===========================================
- Coverage 94.94% 93.02% -1.93%
===========================================
Files 99 22 -77
Lines 4373 1634 -2739
Branches 2168 504 -1664
===========================================
- Hits 4152 1520 -2632
+ Misses 162 66 -96
+ Partials 59 48 -11
... and 77 files with indirect coverage changes Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
jll63
force-pushed
the
feature/reflection-registration
branch
from
August 29, 2026 16:47
6539363 to
095002e
Compare
Two things the formatter was getting wrong, and the version bump that
one of them needs.
`TemplateNames` - added in clang-format 20 - tells the formatter that
`register_classes<` opens a template argument list rather than a
comparison, so a registration inside a function-like macro call breaks
after the open parenthesis instead of aligning at it.
`PenaltyReturnTypeOnItsOwnLine` goes from 60 to 200, so a declaration
too long for one line breaks its parameter list rather than putting
`auto` on a line of its own:
static auto initialize(
const Context& ctx, const std::tuple<Options...>& options) -> void;
rather than
static auto
initialize(const Context& ctx, const std::tuple<Options...>& options)
-> void;
200 is well past the threshold - the layout stops changing above 120 -
and gives what 1000 gives, so the choice is not on a knife edge.
The rest of the diff is the drift from clang-format 18 to 22: trailing
return types and `if constexpr` continuations, mostly. `.clang-format`
now requires clang-format 20 or later, which rejects no key in this
file; 22 reproduces the tree byte for byte.
`register_classes` itself arrives with the reflection branch; naming
it here only teaches the formatter, and costs nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDTJyUvPbFj23o8zVcuvHt
`BreakConstructorInitializers` and `BreakInheritanceList` go from
BeforeColon to AfterColon, so a member initializer list and a base
clause trail their colon rather than lead it, and their continuations
sit on a flat +4 ladder instead of aligning two columns past the
colon:
explicit virtual_ptr(std::nullptr_t) :
vp(detail::box_vptr<use_indirect_vptrs>(detail::null_vptr)),
obj(nullptr) {
rather than
explicit virtual_ptr(std::nullptr_t)
: vp(detail::box_vptr<use_indirect_vptrs>(detail::null_vptr)),
obj(nullptr) {
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDTJyUvPbFj23o8zVcuvHt
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GCC 16 since r16-8246 - the fix for PR124575, "ICE with lifetime extension of
consteval-only" - marks a lifetime-extended temporary of consteval-only type
DECL_EXTERNAL. `vector<meta::info>` is such a type, and the range-for's
__for_range is such a temporary, so the constant evaluator hands every frame of
a recursive consteval call the same object: the inner call destroys the vector
the outer call is still walking.
Both recursive scanners tripped on it, and the whole C++26 leg of the b2 matrix
failed to compile - 37 of 58 test/test_*.cpp, 148 targets:
bits/stl_vector.h:792:29: error: accessing '<anonymous>' outside its lifetime
note: declared here
std::meta::bases_of(type, std::meta::access_context::unchecked())) {
A plain automatic variable is not an extended-ref temporary, so each frame gets
its own. Verified against the exact CI compiler (Ubuntu 16-20260322,
r16-8246): 58/58 compile, where HEAD gave 21/58.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RWrWt9az5Bb9pjP9L9qRz
boost-clone defaults modules-exclude-paths to `test tests`, so it never scans our own test/ and cloned neither library. The job then died at CMake generate, before compiling anything: CMake Warning: Library 'test' given in BOOST_INCLUDE_LIBRARIES has not been found. CMake Error at test/CMakeLists.txt:96: links to Boost::unit_test_framework but the target was not found. The action unions an explicit `modules` with the scan results and resolves their own dependencies afterwards, so naming the two is enough. The Antora jobs use the same action and are unaffected: they never configure a test target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015RWrWt9az5Bb9pjP9L9qRz
r16-8246 is only the snapshot Boost.CI happens to install; the regression starts at d51a78f7a8f3. Isolated by building cc1plus at that commit and at its parent, 95d2eb6fa073, which differ by exactly the four-line DECL_EXTERNAL hunk in set_up_extended_ref_temp: the parent accepts the scan, the commit rejects it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015RWrWt9az5Bb9pjP9L9qRz
The bug is GCC PR124645/PR124646, reported 2026-03-26 and fixed on 2026-04-02 by r16-8430: r16-8235 set DECL_EXTERNAL on the lifetime-extended temporary for both the at_function_scope_p and !at_function_scope_p cases, where only the latter was intended. Naming the PRs and the fixing revision is more use to a future reader than the introducing SHA alone - it says when the workaround can go. Not yet: Ubuntu 26.04, which Boost.CI uses for the C++26 leg, ships 16-20260322 (r16-8246), between the two. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015RWrWt9az5Bb9pjP9L9qRz
`use_classes_in<^^ns, Registry>` took one namespace and one registry. `register_classes<...>` takes four groups of non-type arguments, each optional, in enforced order - namespaces to scan, classes to register, one `register_classes_opts` value, registries (as reflections now, since the pack is `auto...`) - and registers in every listed registry. Listed classes are dispatch roots: registered whether a method dispatches on them or not, along with the scanned classes deriving from them. Classes with no namespace disable the scan - exactly the listed classes are registered, their inheritance lattice read from reflection and flattened over unlisted intermediates. Scans now skip `boost` as well as `std` - worth ~0.2s per TU on a scan of `^^::` in a Boost.Test TU, and reversible with `scan_boost`/`scan_std`; `no_recurse` keeps a scan out of nested namespaces. The options live in a namespace rather than an enum class so a using-directive can make the terse spellings available. With no namespace and no class at all, the enclosing namespace is scanned. Three routes lead there, all resting on P2996's call-site evaluation of `access_context::current()`: the default template argument covers `register_classes<>`; a `detail::scope_marker` that `BOOST_OPENMETHOD_REGISTER_CLASSES` always prepends covers every macro form, registry-only and options-only included; and the public `current_namespace()` helper covers bare-template argument lists, which the template itself cannot capture - a static_assert points there. The macro no longer pastes `^^`; the caller writes it. The two new compile-fail tests produce the expected diagnostic via `#error` when reflection is off, so they pass under every configuration with no build-file changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDTJyUvPbFj23o8zVcuvHt
jll63
force-pushed
the
feature/reflection-registration
branch
from
August 29, 2026 19:30
c63ca00 to
77996d8
Compare
MrDocs' front-end does not implement P2996, so the reference build runs at `-D CMAKE_CXX_STANDARD=20` and sees `BOOST_OPENMETHOD_HAS_REFLECTION` as 0: every `#if BOOST_OPENMETHOD_HAS_REFLECTION` block is invisible to it, and the whole C++26 reflection API was missing from the reference. `register_classes` and `current_namespace` are now restated as documentation stubs in an `#elif defined(__MRDOCS__)` branch beside the real declarations, following `inplace_vptr_derived`: each doc comment exists exactly once, on the stub, and the real declaration carries only a `@see`. `detail/reflection.hpp` forward-declares `std::meta::info` and `std::meta::access_context` under `#ifdef __MRDOCS__` so `current_namespace` can render its exact signature; `include-symbols` keeps them out of the pages. The `register_classes` stub is `template<auto... Args>` where the real one splits `First`/`Rest` - the split exists only because a pack cannot carry a default argument, and MrDocs prints a default argument as raw source text, so the real spelling would leak `detail::`. `register_classes_opts` needs no stub at all: it is plain C++17, so its guard is widened to `|| defined(__MRDOCS__)` and MrDocs reads the real definition. `BOOST_OPENMETHOD_REGISTER_CLASSES` was not extracted either, though for a different reason: its doc comment sat above the `#if`, separated by preprocessor directives from the `#else` `#define` that MrDocs compiles, so nothing attached it. `ref_macros.adoc` has linked to that page all along, and the link rendered as a literal `href="#reference:BOOST_OPENMETHOD_REGISTER_CLASSES.adoc"`. Moving the comment down to the fallback definition produces the page. Rendering those comments for the first time exposed three markup traps, none of which asciidoctor or MrDocs reports: * a line starting with `- ` becomes a list item, so the em-dash before "pass it explicitly" split the paragraph and opened a stray bullet; * `@ref` inside `**bold**` breaks the span, leaving literal asterisks - the options bullet now bolds plain text, like its three siblings; * an inline `` `^^::` `` loses both carets and renders as `::`. Escaping the first, `` `\^^::` ``, comes through intact. Found by building the page with eight candidate spellings; only that one survives. `registries_and_policies.adoc` pointed at `xref:reference:use_classes.adoc`, which resolves only for macros - MrDocs puts those at the top level and namespace-scoped symbols under `reference/boost/openmethod/`. It is now `cpp:use_classes[]`, and the prose that names the new symbols links to them the same way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDTJyUvPbFj23o8zVcuvHt
`register_classes` took a flat list of reflections, sorted by kind and required
to appear in canonical order. The arguments are now groups:
register_classes<{^^zoo, ^^pets}, {^^Animal}, {^^r1, ^^r2}>
A group holds one kind of reflection - the namespaces to scan, the classes to
register, the registries to register them in, in that order - and a group of
one needs no braces, so `register_classes<^^zoo, ^^r1>` still means what it
did. Groups are still identified by what they hold rather than by position, so
any of them may be left out: `register_classes<{^^r1}>` names only registries.
The argument type, `detail::reflection_group`, is a class-type non-type
template parameter with a deduced-class-type placeholder, deducing its length
from the braced template argument through CTAD (P2308). A pack of them replaces
the old `template<auto...>`; `auto` cannot be deduced from a braced-init-list
("unable to deduce 'auto' from '{1, 2}'"). Its converting constructor is what
lets a lone reflection stand for a group of one, `consteval` because
`std::meta::info` is a consteval-only type, and constrained so that it cannot
be picked over the copy constructor. None of this is P2996: with `int` in place
of `std::meta::info` the same shape compiles clean on gcc 13, 15 and 16 and on
clang 18 and 22 at `-std=c++20` with `-pedantic-errors`, and on MSVC v18 at
`/std:c++20` with `/W4 /permissive-`.
The options are gone with `register_classes_opts`, `has_opt` and the `opts`
parameter threaded through the scan. A scan now always recurses, and never
descends into `std` or `boost`; a namespace *listed* explicitly is scanned
whatever it is, which is how a class in either gets registered, and was already
true before. `no_recurse` bought little for a mechanism whose cost is dominated
by the namespaces it is pointed at.
With no namespace group the scan covers `^^::`, where it used to cover the
namespace enclosing the registrar. Listing classes no longer turns the scan
off: they are extra roots, and the classes deriving from them are registered
too. `current_namespace()` survives as the way to narrow a scan to one
namespace, which is the only thing that still needs it.
That default is what removes `detail::scope_marker` and the `scope` member each
group used to carry - a default member initializer calling
`access_context::current()`, evaluated at the initialization site. Without it
`reflection_group` needs no aggregate-ness, which is what made the converting
constructor above possible, and `register_classes` needs no `First`/`Rest`
split: an empty pack already covers `register_classes<>`. Nothing separates
`BOOST_OPENMETHOD_REGISTER_CLASSES` from the bare template now except its
expanding to nothing without reflection.
Three static_asserts diagnose an argument list, ordered so that one mistake
yields one message: an unknown reflection, then a group mixing two kinds, then
groups out of order. `compile_fail_reflection_mixed_group.cpp` covers the
second; `compile_fail_reflection_no_scan_source.cpp` goes, as a registry on its
own is now a scan of `^^::` rather than an error.
`.clang-format` loses `TemplateNames: register_classes`. It was added so that
the flat spelling parsed as a template-id, and it is what turns a braced
argument into `{^^a }`; without it both spellings come out right, and
clang-format 18 can read the file again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDTJyUvPbFj23o8zVcuvHt
Three defects in what `register_classes` gathers, each of them silent, and one
piece of dead code.
**Classes nested in classes were never found.** `scan_namespace` recursed into
nested namespaces only, so `struct Outer { struct Inner : Animal {}; };` was
skipped - and the reference promises "every class in the scanned namespaces
that derives from one of them". An `Inner` dispatched as `Animal&` got
`unknown_class` under `runtime_checks`, and an out-of-range vptr lookup
without them. It is now `scan_scope`, and walks a class as it walks a
namespace. Recursion enters only a class the scope actually *declares*, which
`parent_of` answers: following an alias instead would walk whatever it points
at, and a member `using` for `std::string` would drag the whole of
`basic_string` in behind it.
**cv-qualification was stripped asymmetrically.** `virtual_classes` entries got
`remove_cv`, but the types the scan collected were only `dealias`'d, so a
namespace-scope `using CDog = const Dog` put `const Dog` in the list as an
entry distinct from `Dog` - a second lattice node with its own `static_vptr`,
perfect-hash slot and dispatch table row, for a spelling nothing else uses.
The scan now applies `remove_cv` too.
**Ambiguous bases were documented as left out, but nothing left them out.**
`collect_reflected_bases` only de-duplicated, so with `Left : Animal`,
`Right : Animal`, `Repeated : Left, Right`, `Repeated` was registered under
`Animal` - consuming a class node, a hash slot and dispatch table cells for a
class that can never be passed, as the conversion is ill-formed.
`collect_dispatchable_bases` now drops such a base. Ambiguity is decided by
`is_convertible_type`, which is exact - a virtual base is one subobject however
many paths reach it - but instantiates a template, so it is asked only about
the bases the walk arrived at more than once. A hierarchy without repeated
inheritance instantiates nothing, which is what the cost note in
`reflected_registered_classes_info` asks for.
`reflected_bases_info` and `reflected_bases` were used by nothing in
`include/`, `test/` or `doc/`, and go, along with the mp11 include that only
they needed.
The tests cover the two behaviours that had none: a nested-class fixture
(public and private nesting, plain and cv aliases) and an assertion that the
repeated-inheritance class is absent from the registry. Both were checked
against the unfixed code - the first aborts, the second fails its `BOOST_TEST`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDTJyUvPbFj23o8zVcuvHt
`test/Jamfile` applied `-freflection` on `<toolset>gcc,<cxxstd>26`, with no condition on the compiler version. Every GCC older than 16 accepts `-std=c++26` but rejects the flag - `error: unrecognized command line option` - so `b2 toolset=gcc-14 cxxstd=26`, or any super-project job that adds `cxxstd=26` to a gcc-14 or gcc-15 leg, failed to build the whole test directory, where it had worked before. Pinning the condition to `<toolset>gcc-16` would fix that (b2 registers the major version, so it does match 16.0.1), but it fails in the other direction: a later GCC that no longer needs the flag, or needs it under a different spelling, would silently lose reflection coverage. So this does what the CMake side already does and asks the compiler. `config//has_reflection` compiles a `<meta>` snippet with `-freflection` under whatever standard the build request carries, and `check-target-builds` adds the flag only when that succeeds. The probe is its own project rather than a target under `test/` because a b2 subproject inherits its parent's requirements: declared there, the check target would carry the very conditional it is being consulted for, and asking for it would ask for itself - b2 reports it as an unresolvable reference to `config//has_reflection` from `test/config`. Verified against the three cases: gcc 16 at `cxxstd=26` answers yes and puts the flag on the command line, gcc 16 at `cxxstd=17` answers no, and clang at `cxxstd=26` answers no. gcc 13 does not accept `-std=c++26` at all, so the `[ requires cxx17_... ]` checks skip the directory before this is reached. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDTJyUvPbFj23o8zVcuvHt
Three files traded `BOOST_OPENMETHOD_CLASSES` for the scan on the wrong reasoning, in both directions. `test_virtual_ptr_value_semantics.cpp` registers an extra class between two `initialize` calls, "to make sure dispatch data is not re-constructed in the same place with the same values". That `Cat` is declared inside the test case body, and the file has no `BOOST_OPENMETHOD_REGISTER_CLASSES()`, so under C++26 the macro expanded to nothing and no scan could see a function-local class: nothing was added between the two calls, and the following `p.vptr() != static_vptr<Dog>` was left waiting for an identical rebuild to land at a different address. It goes back to registering by hand, as the sibling registration in the header already does. `test_dispatch_multi.cpp` and `test_namespaces.cpp` went the other way. Both kept a hand registration for `dense_matrix` and `delphinus::Dolphin` on the grounds that "reflection has no way of finding it". Both are namespace-scope classes deriving publicly from a class the methods in the file dispatch on, so the scan finds them exactly like the others; the registration was redundant and the reason given would mislead anyone copying the pattern. They are folded back into the scanned list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDTJyUvPbFj23o8zVcuvHt
…cipe
The snippet header included <boost/openmethod/default_registry.hpp> to get at
`default_registry` before defining the override macro - the pre-core-header
idiom the guidelines drop, and the one thing the documented hand-check looks
for:
git grep -n "openmethod/preamble.hpp\|openmethod/default_registry.hpp" -- doc test
It now has the shape test_capture_errors.hpp uses: forward-declare the
registry, define BOOST_OPENMETHOD_DEFAULT_REGISTRY, include
<boost/openmethod.hpp>, then define the registry. The four error snippets that
include it are unchanged - they include the library themselves afterwards, and
the include guard makes that a no-op - and the check now returns prose only.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDTJyUvPbFj23o8zVcuvHt
`register_classes` documents two ways a namespace member can name a method specialization: a type alias, and a registrar variable whose type is nested in the specialization. Only the first was tested in isolation (`method_only`); every fixture with a registrar also had an alias standing next to it, so a regression in the variable branch of `specialization_named_by` would have been masked by the alias channel in every existing test. `registrar_only` spells the method type out in full everywhere and registers a hand-written overrider with `method<...>::override<...>`: that registrar is the only namespace member whose type involves the specialization, so the test passes only through the variable branch. The alias the test case itself uses to call the method is function-local, which the scan - namespace members only - never sees. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDTJyUvPbFj23o8zVcuvHt
`BOOST_OPENMETHOD_REGISTER_CLASSES()` expands to `register_classes<>`, which
scans the global namespace and names nothing. That is the documented common
case, and the shape some 35 converted tests use - but the empty pack broke two
things in `detail`, both of them invisible under the flags this suite happens
to build with.
`register_classes_groups_are_ordered` formed
`register_classes_kind kinds[] = {register_classes_group_kind<Groups>()...}`,
which is a zero-size array when the pack is empty. That is not standard C++:
GCC rejects it outright under plain `-Wpedantic`, clang under
`-pedantic-errors`. It compiled here only because the suite builds with
`-Wall -Wextra -Werror` and no `-pedantic`, so any consumer that adds it got a
hard error on the library's primary entry point. The empty case is now taken
before the array is formed.
`register_classes_items` reads its `kind` parameter only inside the fold over
`Groups`, which expands to nothing when the pack is empty, so the parameter is
never read and GCC 16 reports it under `-Wunused-but-set-parameter`. That one
was not theoretical: it broke the b2 C++26 leg outright - every test using
`BOOST_OPENMETHOD_REGISTER_CLASSES()` failed to compile under
`toolset=gcc cxxstd=26`, which is `-Werror` here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDTJyUvPbFj23o8zVcuvHt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #89.
When the compiler supports C++26 reflection (P2996), the library can find the
classes taking part in dispatch on its own, and
BOOST_OPENMETHOD_CLASSESbecomes unnecessary in most cases.
What's new
use_classes_in<^^Namespace, Registry>(core.hpp) — a registrar thatscans a namespace by reflection for the classes involved in dispatch, plus
their bases, and registers them. Virtual and multiple inheritance are
supported; unlike
use_classes, repeated inheritance is not an error here —an ambiguous base cannot take part in dispatch, so it is left out.
BOOST_OPENMETHOD_CLASSES_IN(NAMESPACE, ...)(macros.hpp) — the macrowrapper. Reflection sees only what precedes it, so it goes at the bottom of
the file. Without reflection it expands to nothing, so a file that also calls
BOOST_OPENMETHOD_CLASSESbuilds under either standard.policies::explicit_class_registration(preamble.hpp) — opts a registryout, restoring the C++17 behaviour where every class must be registered by
hand. The registry exposes
has_reflected_class_registration. The policy hasno effect if the compiler does not support reflection.
Methods are found through the names that denote them — the
usingdeclarationBOOST_OPENMETHODnow emits, a hand-written one, or any registrar object. Acore-interface method whose
method<...>type is spelled out in full at everyuse, with neither a
usingdeclaration nor an overrider, is named by nothingand is not found; its classes still need
use_classes.Detection is automatic, from
__cpp_impl_reflection— nothing about the C++17build changes.
Tests
test_classes.hppaddsBOOST_OPENMETHOD_TEST_CLASSES, which expands toBOOST_OPENMETHOD_CLASSESin C++17 and to nothing under reflection. Tests thatare not about class registration use it and add a trailing
BOOST_OPENMETHOD_CLASSES_IN(::), so a C++26 run exercises reflection-basedregistration across the whole suite: the classes go unregistered and every test
still has to pass. Tests that check what happens when a class is not
registered keep
BOOST_OPENMETHOD_CLASSESand putexplicit_class_registrationin their registry.test_reflection.cppcovers the scan itself.Build
BOOST_OPENMETHOD_ENABLE_REFLECTION=ONprobes for-std=c++26and-std=c++26 -freflection, and applies whichever works per target (not throughCMAKE_CXX_FLAGS— CMake probes the compiler beforeCMAKE_CXX_STANDARDtakes effect, and GCC rejects
-freflectionunder any other standard).<toolset>gcc,<cxxstd>26:<cxxflags>-freflection.reflectionjob builds and runs the suite with GCC 16.Verified locally in both configurations: C++17/clang 153/153, and C++26
reflection (g++-16
-freflection) 148/148.