Skip to content

register classes by reflection in C++26 - #94

Open
jll63 wants to merge 9 commits into
boostorg:developfrom
jll63:feature/reflection-registration
Open

register classes by reflection in C++26#94
jll63 wants to merge 9 commits into
boostorg:developfrom
jll63:feature/reflection-registration

Conversation

@jll63

@jll63 jll63 commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

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_CLASSES
becomes unnecessary in most cases.

What's new

  • use_classes_in<^^Namespace, Registry> (core.hpp) — a registrar that
    scans 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 macro
    wrapper. 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_CLASSES builds under either standard.
  • policies::explicit_class_registration (preamble.hpp) — opts a registry
    out, restoring the C++17 behaviour where every class must be registered by
    hand. The registry exposes has_reflected_class_registration. The policy has
    no effect if the compiler does not support reflection.

Methods are found through the names that denote them — the using declaration
BOOST_OPENMETHOD now emits, a hand-written one, or any registrar object. A
core-interface method whose method<...> type is spelled out in full at every
use, with neither a using declaration nor an overrider, is named by nothing
and is not found; its classes still need use_classes.

Detection is automatic, from __cpp_impl_reflection — nothing about the C++17
build changes.

Tests

test_classes.hpp adds BOOST_OPENMETHOD_TEST_CLASSES, which expands to
BOOST_OPENMETHOD_CLASSES in C++17 and to nothing under reflection. Tests that
are not about class registration use it and add a trailing
BOOST_OPENMETHOD_CLASSES_IN(::), so a C++26 run exercises reflection-based
registration 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_CLASSES and put
explicit_class_registration in their registry.

test_reflection.cpp covers the scan itself.

Build

  • CMake: BOOST_OPENMETHOD_ENABLE_REFLECTION=ON probes for -std=c++26 and
    -std=c++26 -freflection, and applies whichever works per target (not through
    CMAKE_CXX_FLAGS — CMake probes the compiler before CMAKE_CXX_STANDARD
    takes effect, and GCC rejects -freflection under any other standard).
  • b2: <toolset>gcc,<cxxstd>26:<cxxflags>-freflection.
  • CI: a new reflection job 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.

@cppalliance-bot

cppalliance-bot commented Aug 25, 2026

Copy link
Copy Markdown

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-29 19:36:06 UTC

@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.17355% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 93.02%. Comparing base (b3bec3b) to head (77996d8).
⚠️ Report is 1 commits behind head on develop.

Files with missing lines Patch % Lines
include/boost/openmethod/core.hpp 98.07% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@             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     
Files with missing lines Coverage Δ
include/boost/openmethod/default_registry.hpp 100.00% <ø> (ø)
include/boost/openmethod/detail/ostdstream.hpp 96.77% <100.00%> (+0.10%) ⬆️
include/boost/openmethod/detail/static_list.hpp 96.22% <ø> (ø)
include/boost/openmethod/initialize.hpp 92.02% <100.00%> (+0.03%) ⬆️
include/boost/openmethod/inplace_vptr.hpp 100.00% <100.00%> (ø)
include/boost/openmethod/interop/boost_any.hpp 100.00% <ø> (ø)
...e/boost/openmethod/interop/boost_intrusive_ptr.hpp 94.44% <ø> (ø)
...de/boost/openmethod/interop/boost_type_erasure.hpp 100.00% <100.00%> (ø)
include/boost/openmethod/interop/std_any.hpp 100.00% <ø> (ø)
...nclude/boost/openmethod/interop/std_shared_ptr.hpp 94.73% <ø> (ø)
... and 6 more

... and 77 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update dd2c86f...77996d8. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jll63
jll63 force-pushed the feature/reflection-registration branch from 6539363 to 095002e Compare August 29, 2026 16:47
jll63 and others added 9 commits August 29, 2026 14:53
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
jll63 force-pushed the feature/reflection-registration branch from c63ca00 to 77996d8 Compare August 29, 2026 19:30
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.

auto-register classes in C++26

2 participants