Skip to content

registry affinity: a registry parameter for virtual_, and class lists that follow it - #113

Merged
jll63 merged 1 commit into
boostorg:developfrom
jll63:feature/virtual-registry-param
Sep 17, 2026
Merged

jll63 merged 1 commit into
boostorg:developfrom
jll63:feature/virtual-registry-param

Conversation

@jll63

@jll63 jll63 commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

(Written by Claude Code, on behalf of @jll63.)

Two loose ends from #96, settled by one rule.

virtual_ptr<C, S> carries a registry, but the scan that picks a method's
registry read the class's declared affinity instead of what the parameter
carried, so BOOST_OPENMETHOD(f, (virtual_ptr<B, a_registry>), void) was an
error: the scan took default_registry from B, then rejected the parameter
for carrying a_registry. And virtual_<T> could not name a registry at all.

Every virtual parameter carries a registry, or adopts

virtual_ takes a registry parameter — declared in preamble.hpp, defaulted in
core.hpp where the affinity is known, since C++ merges default template
arguments across declarations.

carries
virtual_<T> its class's declared affinity; adopts if the class declares none
virtual_<T, S> S
virtual_ptr<C> C's affinity, else default_registry
virtual_ptr<C, S> S

virtual_ptr never adopts — it is a type in its own right and names a registry
whether or not its class declares an affinity. virtual_ can, because it
appears only in a method signature, and that is what lets a method mix a class
that has an affinity with one that has not.

A method that names a registry requires every carrier to carry it, and the
adopters go along. A method that names none requires the carriers to agree, and
takes their registry. So method<foo, void(virtual_<A&, default_registry>)> is
method<foo, void(virtual_<A&, default_registry>), default_registry>.

With A declaring a_registry and B declaring nothing:

parameter no registry , a_registry , default_registry
virtual_<A&> a_registry ok error
virtual_<B&> default_registry ok ok
virtual_<B&, a_registry> a_registry ok error
virtual_ptr<A> a_registry ok error
virtual_ptr<B> default_registry error ok
virtual_ptr<B, a_registry> a_registry (was an error) ok error
virtual_ptr<A, default_registry> default_registry (was an error) error ok

Nothing that compiles today changes meaning; the two marked rows gain one.

The sentinel for "carries nothing" is void, replacing the default_affinity
struct — as inplace_vptr's private catch-all spelled it before #96 unified the
hook. It keeps the mangled names short, now that it appears in every
virtual_<T, …> of every method<…>. registry_affinity still maps it to
BOOST_OPENMETHOD_DEFAULT_REGISTRY; only ::declared sees it raw. A member
typedef using boost_openmethod_registry = void; therefore means "declares
nothing".

Class lists follow the affinities too

use_classes took the registry from the last element, or else the macro
default, whatever the classes said — so registering a class that declares an
affinity, without naming its registry, put the class in one registry and its
methods in another. Not a compile error, a crash:

BOOST_OPENMETHOD_CLASSES(Animal, Dog);            // Animal declares zoo_registry
BOOST_OPENMETHOD(speak, (virtual_<const Animal&>), std::string);
initialize<zoo_registry>();
speak(dog);
before:  unknown class Animal → Aborted (core dumped), exit 134
after:   bark, exit 0

A class list is stricter than a parameter list, because it has no parameter to
adopt from and no spelling of its own to disambiguate with:

outcome
use_classes<A, A1> a_registryA1 inherits the declaration
use_classes<B> default_registry
use_classes<A, A1, B, a_registry> a_registryB declares nothing, so it goes along
use_classes<A, B> error — some declare, some do not
use_classes<A> a_registry (was default_registry, silently)
use_classes<A, default_registry> error (was allowed, silently)

Mixing a declaring class with a non-declaring one is an error, where the same
mixture among a method's parameters is fine. The C++26 reflection registrar
keeps the macro default: its groups may name a namespace, whose classes are
only known during the scan that the choice of registry feeds.

Tests

compile_fail_adl_registry_parameter_registry.cpp is removed — its premise,
that a registry spelled on a parameter contradicts the method, is what this
reverses. Two markers move to the fold's new wording. Four compile-fail tests
are added, one per new diagnosis, and the carries/adopts table and the six
class-list outcomes above are pinned with static_asserts.

189/189 under gcc 13.3 Debug with warnings as errors, 195/195 under gcc 16 with
C++26 reflection and BUILD_SHARED_LIBS=ON, and every compile-fail diagnostic
was matched under clang 18.1 as well.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RQG6CbE4o2agseE7bDVzHS

@cppalliance-bot

cppalliance-bot commented Sep 16, 2026

Copy link
Copy Markdown

An automated preview of the documentation is available at https://113.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-09-16 22:12:13 UTC

@codecov

codecov Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.25%. Comparing base (d44aa59) to head (219e921).
⚠️ Report is 6 commits behind head on develop.

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop     #113      +/-   ##
===========================================
- Coverage    93.51%   93.25%   -0.26%     
===========================================
  Files           22       22              
  Lines         1695     1706      +11     
  Branches       504      509       +5     
===========================================
+ Hits          1585     1591       +6     
- Misses          66       71       +5     
  Partials        44       44              
Files with missing lines Coverage Δ
include/boost/openmethod/core.hpp 91.60% <ø> (-1.49%) ⬇️
include/boost/openmethod/preamble.hpp 78.57% <ø> (ø)

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 bcc075d...219e921. 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/virtual-registry-param branch 2 times, most recently from 80fce95 to 80823f5 Compare September 16, 2026 14:18
… that follow it

Two loose ends from boostorg#96, and one rule for both.

`virtual_ptr<C, S>` carries a registry, but the scan that picks a method's
registry read the *class's* declared affinity instead of what the parameter
carried. `BOOST_OPENMETHOD(f, (virtual_ptr<B, a_registry>), void)` was
therefore an error: the scan took `default_registry` from `B`, then rejected
the parameter for carrying `a_registry`. And `virtual_<T>` could not name a
registry at all.

`virtual_` now takes one - `virtual_<T, S>` - declared in `preamble.hpp` and
defaulted in `core.hpp`, where the affinity is known; C++ merges default
template arguments across declarations. Every virtual parameter then either
*carries* a registry or *adopts* the method's:

    virtual_<T>          the class's declared affinity; adopts if it declares none
    virtual_<T, S>       S
    virtual_ptr<C>       C's affinity, else default_registry
    virtual_ptr<C, S>    S

`virtual_ptr` never adopts - it is a type in its own right and names a registry
whether or not the class declares an affinity. `virtual_` can, because it
appears only in a method signature, and that is what lets a method mix a class
that has an affinity with one that has not.

A method that names a registry requires every carrier to carry it; the adopters
go along. A method that names none requires the carriers to agree, and takes
their registry. So `method<foo, void(virtual_<A&, default_registry>)>` is
`method<foo, void(virtual_<A&, default_registry>), default_registry>`.

Nothing that compiles today changes meaning. Two spellings gain one:
`virtual_ptr<B, a_registry>` and `virtual_ptr<A, default_registry>` as the sole
virtual parameter now decide the method's registry.

The sentinel for "carries nothing" is `void`, replacing the `default_affinity`
struct - as `inplace_vptr`'s private catch-all spelled it before boostorg#96 unified
the hook. It keeps the mangled names short, now that it appears in every
`virtual_<T, ...>` of every `method<...>`. `registry_affinity` still maps it to
BOOST_OPENMETHOD_DEFAULT_REGISTRY; only `::declared` sees it raw. A member
typedef `using boost_openmethod_registry = void;` therefore means "declares
nothing".

`use_classes` and `BOOST_OPENMETHOD_CLASSES` follow the affinities too, where
they used to take the registry from the last element or else the macro default,
whatever the classes said - so registering a class that declares an affinity,
without naming its registry, put the class in one registry and its methods in
another. That was a run-time failure, not a compile error:

    BOOST_OPENMETHOD_CLASSES(Animal, Dog);            // Animal declares zoo_registry
    BOOST_OPENMETHOD(speak, (virtual_<const Animal&>), std::string);
    initialize<zoo_registry>();
    speak(dog);        // before: unknown class Animal, abort; after: "bark"

A class list is stricter than a parameter list, because it has no parameter to
adopt from and no spelling of its own to disambiguate with. A registry listed
last wins, and then a class declaring another one is an error while one
declaring nothing goes along. Listing none, the classes must be unanimous - all
declaring the same registry, or none declaring one, in which case they are
registered into BOOST_OPENMETHOD_DEFAULT_REGISTRY. Mixing a declaring class
with a non-declaring one is an error, where the same mixture among a method's
parameters is fine. The C++26 reflection registrar keeps the macro default: its
groups may name a namespace, whose classes are only known during the scan that
the choice of registry feeds.

compile_fail_adl_registry_parameter_registry.cpp goes: its premise - that a
registry spelled on a parameter contradicts the method - is what this reverses.
Two markers move to the fold's new wording. Four compile-fail tests are added,
one per new diagnosis, and the carries/adopts table and the six class-list
outcomes are pinned with static_asserts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RQG6CbE4o2agseE7bDVzHS
@jll63
jll63 force-pushed the feature/virtual-registry-param branch from 80823f5 to 219e921 Compare September 16, 2026 22:05
@jll63
jll63 merged commit 99e6d00 into boostorg:develop Sep 17, 2026
54 of 55 checks passed
jll63 added a commit that referenced this pull request Sep 17, 2026
#113 gave `virtual_` a registry parameter, so that a method can be
declared without naming a registry and a parameter can name one. The
`validate_method_parameter` specializations in the `any` and
type_erasure interop headers were left spelling `virtual_<T>`, which
after the change matches only the defaulted second argument. A method
declared `virtual_<const std::any&, R>` - valid, and what the registry
affinity machinery produces when a parameter names its registry - fell
through to the primary template instead:

    error: virtual_<> parameter is not a polymorphic class and no
    boost_openmethod_vptr is applicable

Give the registry `virtual_` carries its own template parameter, as the
specializations in core.hpp do. The rejecting one, for an owning
`type_erasure::any` passed by value, is fixed the same way, so that it
keeps reporting "an owning type_erasure::any must be passed by
reference" rather than the vague message above.

The three dispatch tests assert the accepting spellings, in each form
the header covers.


Claude-Session: https://claude.ai/code/session_018FzFtVWNk6d4Wut5SboCZC

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jll63
jll63 deleted the feature/virtual-registry-param branch September 18, 2026 01:26
jll63 added a commit to jll63/openmethod that referenced this pull request Sep 18, 2026
…g#116)

boostorg#113 gave `virtual_` a registry parameter, so that a method can be
declared without naming a registry and a parameter can name one. The
`validate_method_parameter` specializations in the `any` and
type_erasure interop headers were left spelling `virtual_<T>`, which
after the change matches only the defaulted second argument. A method
declared `virtual_<const std::any&, R>` - valid, and what the registry
affinity machinery produces when a parameter names its registry - fell
through to the primary template instead:

    error: virtual_<> parameter is not a polymorphic class and no
    boost_openmethod_vptr is applicable

Give the registry `virtual_` carries its own template parameter, as the
specializations in core.hpp do. The rejecting one, for an owning
`type_erasure::any` passed by value, is fixed the same way, so that it
keeps reporting "an owning type_erasure::any must be passed by
reference" rather than the vague message above.

The three dispatch tests assert the accepting spellings, in each form
the header covers.


Claude-Session: https://claude.ai/code/session_018FzFtVWNk6d4Wut5SboCZC

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
jll63 added a commit to jll63/openmethod that referenced this pull request Sep 19, 2026
Both were falsified by changes that did not update the prose around them.

boostorg#113 collapsed the "Two things deliberately do not participate" bullets
into one sentence, because `use_classes` had just started participating -
but then named two things under "One thing": the interop headers and the
C++26 `register_classes`. Restore the bullet form for the two that are
left.

The same sentence says the `any` and `type_erasure` interop headers "are
untouched". boostorg#116 touched all three of them, and had to: boostorg#113 gave
`virtual_` a registry parameter, and their `validate_method_parameter`
specializations still spelled `virtual_<T>`, which after the change
matches only the defaulted argument. What survives is the affinity claim,
which is the point of the paragraph - a `virtual_any` contributes none.
Say that, and record why a specialization there cannot go back to the
bare spelling.

The PCH paragraph names `test_capture_errors.hpp` as the only header that
carries the override on a test's behalf. `test/CMakeLists.txt` has
scanned for `test_checked_registry.hpp` as well since boostorg#93; the sentence
three lines above, "do not add a fourth marker", already counts three.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jll63 added a commit to jll63/openmethod that referenced this pull request Sep 19, 2026
Both were falsified by changes that did not update the prose around them.

boostorg#113 collapsed the "Two things deliberately do not participate" bullets
into one sentence, because `use_classes` had just started participating -
but then named two things under "One thing": the interop headers and the
C++26 `register_classes`. Restore the bullet form for the two that are
left.

The same sentence says the `any` and `type_erasure` interop headers "are
untouched". boostorg#116 touched all three of them, and had to: boostorg#113 gave
`virtual_` a registry parameter, and their `validate_method_parameter`
specializations still spelled `virtual_<T>`, which after the change
matches only the defaulted argument. What survives is the affinity claim,
which is the point of the paragraph - a `virtual_any` contributes none.
Say that, and record why a specialization there cannot go back to the
bare spelling.

The PCH paragraph names `test_capture_errors.hpp` as the only header that
carries the override on a test's behalf. `test/CMakeLists.txt` has
scanned for `test_checked_registry.hpp` as well since boostorg#93; the sentence
three lines above, "do not add a fourth marker", already counts three.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jll63 added a commit that referenced this pull request Sep 19, 2026
…e notes (#118)

* doc: correct two stale notes in CLAUDE.md's registry-affinity section

Both were falsified by changes that did not update the prose around them.

#113 collapsed the "Two things deliberately do not participate" bullets
into one sentence, because `use_classes` had just started participating -
but then named two things under "One thing": the interop headers and the
C++26 `register_classes`. Restore the bullet form for the two that are
left.

The same sentence says the `any` and `type_erasure` interop headers "are
untouched". #116 touched all three of them, and had to: #113 gave
`virtual_` a registry parameter, and their `validate_method_parameter`
specializations still spelled `virtual_<T>`, which after the change
matches only the defaulted argument. What survives is the affinity claim,
which is the point of the paragraph - a `virtual_any` contributes none.
Say that, and record why a specialization there cannot go back to the
bare spelling.

The PCH paragraph names `test_capture_errors.hpp` as the only header that
carries the override on a test's behalf. `test/CMakeLists.txt` has
scanned for `test_checked_registry.hpp` as well since #93; the sentence
three lines above, "do not add a fourth marker", already counts three.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* doc: state the rule against committing build output

#103 merged 415 files of b2 output - 613 MiB expanded, 94 MiB in the
pack, on a repository of about 3 MB - days after #108 added the `bin/`
ignore rule intended to prevent it. The branch was cut before that rule
landed, and an ignore rule does not apply to a path that is already
tracked, so the merge carried them in.

Nothing in the build or the review catches this, and #117 could only
untrack them: the superproject pins this library by SHA and its bot
bumps the pin within minutes of every merge, so rewriting `develop`
orphans commits `boostorg/boost` already points at - and would not even
remove the blobs, which stay reachable through `refs/pull/<n>/head`.

Write the rule down where the workflow is: stage named paths, never
`git add <dir>`, and ask before committing a build artefact or any file
over 1MB.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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