Skip to content

classes declare the registry they belong to - #96

Merged
jll63 merged 5 commits into
boostorg:developfrom
jll63:feature/adl-default-registry
Sep 15, 2026
Merged

jll63 merged 5 commits into
boostorg:developfrom
jll63:feature/adl-default-registry

Conversation

@jll63

@jll63 jll63 commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

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

Closes #82.

A class can name the registry it belongs to, once, next to itself:

class Animal {
    friend auto boost_openmethod_registry(Animal*) -> zoo_registry;
};

or, equivalently, with a member typedef:

struct Node {
    using boost_openmethod_registry = nodes_registry;
};

The class then has an affinity for that registry, and everything that mentions
it finds it: virtual_ptr, its two deduction guides, final_virtual_ptr, the
smart pointer aliases and their make_*_virtual factories, and any method that
takes the class as a virtual parameter. A method declared without a registry
argument takes the affinity its virtual parameters agree on.

BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat, zoo_registry);

// no registry argument: speak follows Animal
BOOST_OPENMETHOD(speak, (virtual_<const Animal&>), std::string);

static_assert(std::is_same_v<virtual_ptr<Dog>, virtual_ptr<Dog, zoo_registry>>);

registry_affinity<T> reads it back.

Notes on the shape

Two spellings, typedef first. The function is found by argument-dependent
lookup, which is what lets one declaration cover a hierarchy and lets a class
declare an affinity for a class it does not own. It is also ADL's weaknesses: an
overload outside the class's namespace is silently ignored, a translation unit
that does not include it compiles the same method into another registry, and a
namespace-wide function template outranks an inherited affinity. The member
typedef has none of those - it is ordinary member lookup, so it is inherited,
hidden by a derived class's own, and ambiguous between two bases that disagree -
and it is visible from the point it is declared, which is what a class that
mentions virtual_ptr of itself in its own body needs. Such a class has already
decided to be openmethod-aware; the typedef intrudes no further.

Class*, not Class& as sketched in the issue. It matches the hook
inplace_vptr.hpp already had, so those classes get the behaviour for free;
pointers pass through the ... fallback safely, where a class lvalue is only
conditionally supported; and an overload on Base* ranks below an exact
Derived* one, which is what lets one declaration cover a hierarchy while a
derived class can still override it.

The catch-all returns a sentinel, detail::default_affinity, not a
registry. "Declares nothing" and "declares an affinity for the default registry"
are then distinct: the second constrains a method like any other declaration,
where recovering "no opinion" by comparing with the macro would have made it
yield. registry_affinity maps the sentinel to
BOOST_OPENMETHOD_DEFAULT_REGISTRY and remains a query that always answers a
registry.

No affinity is not an affinity for the default registry. Only a declared one
constrains, so a method may mix a class that declares one with a class that does
not - a first affinity does not cascade errors through a codebase. Two
conflicting affinities are diagnosed, as is a registry named on a method that
contradicts one of its parameters, in any of the four parameter shapes. A
virtual_ptr<Class, R> parameter contributes its class's affinity, never the
R it spells; the two must agree.

A class whose base's overload cannot be used is diagnosed, not defaulted. An
ambiguous or inaccessible base conversion is a substitution failure, so a class
with two differently-hooked bases, a private hooked base, or a repeated
non-virtual hooked base gets a message telling it to declare its own.

The answer is memoized, and asked again where it matters. registry_affinity
is a class template specialization: asked once per class, then remembered. A
class mentioned before it is complete - virtual_ptr<Node> as a member of
Node, or through a forward declaration - is asked before a declaration further
down its body, or a base class, can be seen. Refusing to answer is not an
option: virtual_ptr<Node> next; in a plain linked structure declares no
affinity, needs none, and has worked since #109. So the question is tagged. The
answer that builds types is the one above; check_affinity puts the same
question again where the class must be complete anyway - a virtual parameter, a
class registration - and refuses an answer that has changed. Declaring nothing
stays silent; declaring too late is an error, reported where a wrong registry
would do its damage rather than at the mention, where nothing is wrong yet.

Compatibility

With nothing declared anywhere the catch-all answers for every class, so
registry_affinity<T> is BOOST_OPENMETHOD_DEFAULT_REGISTRY and nothing
changes. Measured rather than argued: for test_virtual_ptr_dispatch.cpp at
-O2 -g0, .text is byte-identical and all 530 defined symbols are unchanged.
No existing spelling changes meaning or stops compiling, including
BOOST_OPENMETHOD(..., R) over an unhooked virtual_<T>.

Deliberately out of scope

  • virtual_ keeps its single template parameter. Giving it a Registry would
    cost a mangled-name change for every method<...> mentioning it, nine
    pattern-match edits in core.hpp, and twelve specializations in the any and
    type_erasure headers. Scanning the parameter list reaches the same place.
  • use_classes / BOOST_OPENMETHOD_CLASSES still register into the macro
    default unless a registry is listed last. Registering a class that has an
    affinity without naming its registry is a run-time missing_class, not a
    compile error - documented with a warning, and the obvious follow-up. The
    C++26 reflection registrar has the same fallback.
  • The any and type_erasure interop headers are untouched.

Tests and docs

Eight tests and eleven compile-fail tests, one per diagnosis. 185/185 pass under
gcc 13.3 Release with examples, and 191/191 under gcc 16 with C++26 reflection
and BUILD_SHARED_LIBS=ON, both with BOOST_OPENMETHOD_WARNINGS_AS_ERRORS=ON;
every compile-fail diagnostic was also matched under clang 18.1. Documentation
introduces "registry affinity" as the term, in registries_and_policies.adoc,
with a new example; mrdocs.yml stops excluding the hook, so it and
registry_affinity now have reference pages.

Since the first review round

Rebased onto current develop. A multi-angle review of the branch found five
ways the affinity was answered wrongly or unchecked, each reproduced on gcc 13,
clang 18 and MSVC, and all five are fixed in the last commit: the memoization
above; an anchor that unwrapped anything with a nested element_type, so a
polymorphic class defining one lost its affinity (it now goes through
virtual_traits); a return type taken verbatim, so -> const zoo_registry was
a distinct registry with its own state and -> int failed far from the
declaration; the sentinel above; and a mismatch check that covered only one of
the four parameter shapes. The virtual_ptr part of that work - a constraint
that demanded a complete class too early - was split out and merged separately
as #109.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RQG6CbE4o2agseE7bDVzHS

@jll63 jll63 changed the title Virtual parameters obtain their registry by ADL virtual parameters obtain their registry by ADL Sep 1, 2026
@cppalliance-bot

cppalliance-bot commented Sep 1, 2026

Copy link
Copy Markdown

An automated preview of the documentation is available at https://96.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-15 01:51:55 UTC

@jll63 jll63 changed the title virtual parameters obtain their registry by ADL virtual parameters obtain their registry via ADL Sep 1, 2026
@jll63 jll63 closed this Sep 2, 2026
@jll63 jll63 reopened this Sep 2, 2026
@jll63
jll63 force-pushed the feature/adl-default-registry branch from 60a423f to 0ef01ba Compare September 2, 2026 13:50
@codecov

codecov Bot commented Sep 2, 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 (ab876ed).
⚠️ Report is 1 commits behind head on develop.

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop      #96      +/-   ##
===========================================
- 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% <100.00%> (-1.49%) ⬇️
include/boost/openmethod/inplace_vptr.hpp 100.00% <ø> (ø)
...e/boost/openmethod/interop/boost_intrusive_ptr.hpp 94.44% <ø> (ø)
...nclude/boost/openmethod/interop/std_shared_ptr.hpp 94.73% <ø> (ø)
...nclude/boost/openmethod/interop/std_unique_ptr.hpp 90.00% <ø> (ø)

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 10bdfe5...ab876ed. 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/adl-default-registry branch from 0ef01ba to 55b4446 Compare September 2, 2026 22:58
@jll63
jll63 force-pushed the feature/adl-default-registry branch 2 times, most recently from 55b4446 to ef84783 Compare September 10, 2026 23:52
jll63 and others added 4 commits September 14, 2026 17:09
A class can now name the registry it belongs to, once, next to itself:

    class Animal {
        friend auto boost_openmethod_registry(Animal*) -> zoo_registry;
    };

The class then has an *affinity* for that registry, inherited by its derived
classes, and everything that mentions it finds it: `virtual_ptr`, its deduction
guides, `final_virtual_ptr`, the smart pointer aliases and factories, and any
method that takes the class as a virtual parameter. A method declared without a
registry argument takes the affinity its virtual parameters agree on.

`inplace_vptr.hpp` already had this hook, privately, returning `void` to mean
"no customization". Making the catch-all return BOOST_OPENMETHOD_DEFAULT_REGISTRY
instead lets it serve as a default template argument directly, and makes
backward compatibility structural: with no overload anywhere, every construct
resolves to what it resolved to before. `.text` for test_virtual_ptr_dispatch.cpp
is byte-identical, and the 530 defined symbols are unchanged.

Having *no* affinity is not the same as an affinity for the default registry -
only the former yields. That is what lets a method mix a class that has one with
a class that has none, so a first affinity does not cascade errors through a
codebase. Two conflicting affinities are diagnosed, as is a registry named on a
method that contradicts one of its parameters.

Deliberately out of scope, and documented as such: `virtual_` keeps its single
template parameter; `use_classes` still registers into the macro default unless
a registry is listed last; the `any` and `type_erasure` interop headers are
untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoMTqq3duJAptRAbCXgNh1
compile_fail_adl_registry_declared_mismatch only declared the method. The
"registry mismatch" static_assert is a fold in `method`'s class body, so it
fires when the class is instantiated - and declaring the method is not enough.
gcc and clang instantiate it anyway through the static registrar; MSVC does
not, so the file compiled and the compile-fail test failed on every Windows
job.

Call the method in main(), the way
compile_fail_virtual_ptr_different_registries.cpp already does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoMTqq3duJAptRAbCXgNh1
"Affinity" is the term the documentation uses for the relation, so the query
that reads it back should carry it too. `default_registry_of` also read badly
where it mattered most: "Registry defaults to the default registry of Class".

`affine_registry` was the other candidate and is worse - it predicates "affine"
of the registry, when it is the class that has the affinity, and "affine" reads
as affine geometry in a library whose flagship example dispatches on matrix
types.

The concept is adjusted to match the name. Every class now *has* a registry
affinity: a declared one if it declares `boost_openmethod_registry`, the default
affinity otherwise. A declared affinity wins over a default one, which is the
same rule as before - a method may mix a class that declares an affinity with
one that does not - but stated without the awkward "no affinity, which is not
the same as an affinity for the default registry". It also removes a corner that
framing had: a class declared explicitly to the default registry is no longer a
special case, it simply has the default affinity like any other.

detail::affinity_of becomes declared_affinity, and no_affinity becomes
default_affinity, so the internals read the same way as the prose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoMTqq3duJAptRAbCXgNh1
develop adopted clang-format 22 in 6b02978; reflow the files this branch
touches to match, so the diff carries no formatting noise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoMTqq3duJAptRAbCXgNh1
@jll63
jll63 force-pushed the feature/adl-default-registry branch from ef84783 to cb84d19 Compare September 15, 2026 01:36
@jll63 jll63 changed the title virtual parameters obtain their registry via ADL classes declare the registry they belong to Sep 15, 2026
…ered

Review of this branch turned up five ways `registry_affinity` gives a wrong or
unchecked answer, each reproduced on gcc 13, clang 18 and MSVC.

**Memoization.** `registry_affinity<Class>` is a class template specialization:
asked once, then remembered. A class mentioned before it is complete - a
`virtual_ptr<Cat>` named after `class Cat;`, or `virtual_ptr<Node>` as a member
of `Node` - is asked before its base classes, or a declaration further down its
body, can be seen. The answer, the default registry, then stuck for the whole
translation unit: a method over the class landed in one registry while
`BOOST_OPENMETHOD_CLASSES(..., zoo_registry)` registered it in another, and two
translation units that completed the class in different orders saw two
`virtual_ptr<Cat>` types - `nm` showed `U feed(virtual_ptr<Dog,
default_registry>)` in one object against `T feed(virtual_ptr<Dog,
zoo_registry>)` in the other. The doc said the opposite: "Being part of the
class, a hidden friend cannot be late."

Refusing to answer an incomplete class is not open to us: `virtual_ptr<Node>
next;` in a plain linked structure declares no affinity, needs none, and has
worked since boostorg#109. So every question now carries a tag. The `asked` one builds
types, as before; `check_affinity<Class>` puts the same question again under
`rechecked` where the class must be complete anyway - a virtual parameter, in
`validate_method_parameter`, and a registration, in `use_class_aux` - and
refuses an answer that has changed. Declaring nothing stays silent; declaring
too late is an error, reported where the wrong registry would do its damage
rather than at the mention, where nothing is wrong yet.

**The anchor.** `registry_anchor` unwrapped anything with a nested
`element_type`, so a polymorphic class that happens to define one - a matrix, a
buffer - lost its own declared affinity; an `inplace_vptr` class with one
registered into the wrong registry and stored that registry's null
`static_vptr` in the object. A smart pointer is now a type `virtual_traits` is
specialized for: `detail::virtual_type<T, macro_default_registry>`, no
`virtual_type` in the tree depending on the registry.

**The return type** was taken verbatim: `-> const zoo_registry` yielded a
distinct registry, with its own `registry_state`, disjoint from the one the
classes were registered in; `-> int` failed at the first
`sizeof(virtual_ptr<Animal>)`, far from the declaration. It is stripped of
cv-qualifiers and checked with `is_registry`.

**The sentinel.** The catch-all returned `BOOST_OPENMETHOD_DEFAULT_REGISTRY`
itself, so "no opinion" had to be recovered by comparing with the macro. An
affinity explicitly declared for the default registry was therefore treated as
none and yielded in a mixed method, and a registry spelled on a `virtual_ptr`
parameter counted as a declared affinity - so a method over `virtual_ptr<Cat,
other_registry>` naming no registry, formerly the `registry mismatch` error,
silently landed in `other_registry`. The catch-all now returns
`detail::default_affinity`. `declared` is the raw answer and constrains a
method; `registry_affinity` maps the sentinel to the macro default and stays a
query that always answers a registry. A `virtual_ptr` parameter contributes its
class's affinity, never the registry it spells.

**The mismatch check** covered only a by-value `virtual_ptr`: `virtual_<const
Animal&>` and `const virtual_ptr<Animal>&` contradictions compiled clean. All
four shapes are checked now.

Along the way, a class can declare its affinity with `using
boost_openmethod_registry = R;`. Member lookup finds it: inherited, hidden by a
derived class's own, ambiguous between two bases that disagree. It takes
precedence over the overload, and being visible from the point it is declared,
it is the spelling for a class that mentions `virtual_ptr` of itself in its own
body - which has already decided to be openmethod-aware, so the typedef
intrudes no further. `inplace_vptr_base` provides it. It also sidesteps the
drawbacks of a free function found only by ADL: a wrong namespace, a
translation unit that does not see it, a function template that outranks it.

Nine compile-fail tests, one per diagnosis.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RQG6CbE4o2agseE7bDVzHS
@jll63
jll63 force-pushed the feature/adl-default-registry branch from cb84d19 to ab876ed Compare September 15, 2026 01:47
@jll63
jll63 merged commit 28494b6 into boostorg:develop Sep 15, 2026
54 of 56 checks passed
jll63 added a commit that referenced this pull request Sep 15, 2026
…thrice (#111)

Follow-ups from the review of #96, all mechanical.

The reference still described the pre-affinity default in five places: the
`BOOST_OPENMETHOD_DEFAULT_REGISTRY` page ("the default value for the Registry
template parameter of method, use_classes, virtual_ptr, and all the constructs
that take a registry" - only `use_classes` is still true), the `virtual_ptr`
class, the one-argument `final_virtual_ptr`, and the `@note` on each of
`BOOST_OPENMETHOD` and `BOOST_OPENMETHOD_CLASSES`. Each now says what the
construct actually defaults to, and the `BOOST_OPENMETHOD_CLASSES` note says
plainly that it does *not* consult affinities - the trap the page warns about.
`registries_and_policies.adoc` gets the same correction in its opening
paragraph, and `class method`'s own description still carried a paragraph
contradicting its `@tparam Registry`.

That description was also missing from the reference entirely: MrDocs does not
attach a `//!` comment to a declaration when a namespace definition comes
between them, and the `namespace detail { ... }` holding the affinity scan had
been inserted there. The block moves above the comment, which now touches
`class method` as it must. Rendered `method` page keeps its call semantics and
overrider-selection steps.

`BOOST_OPENMETHOD_TYPE` expanded `va_args<__VA_ARGS__>` three times and the
parameter list twice, to compute a registry and hand it back to `method`:
30-36% more preprocessor output per declaration, measured with `g++ -E -P`. A
`method_type` alias on the two `va_args` specializations names the method
directly, so the macro spells `va_args` twice and the parameter list once, and
the affinity scan still never runs for a declaration that names a registry.
`va_args::registry` had no consumer left in the tree and goes.

Also: three explanatory comments inside the tagged regions of
`adl_registry.cpp` moved to the page that includes them, per the rule in
CLAUDE.md, and two tests stop naming `BOOST_OPENMETHOD_DEFAULT_REGISTRY` in a
comment - `test/CMakeLists.txt` scans for that token to decide which tests must
not get the shared PCH, and a mention in prose was enough to withhold it.


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

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
jll63 added a commit that referenced this pull request Sep 16, 2026
…#112)

Since #96 a class can declare a registry of its own through
`boost_openmethod_registry`, so an overrider whose class resolves to a
different registry than the method's has parameters that simply do not
convert - `virtual_ptr<Poodle, kennel_registry>` against
`virtual_ptr<Animal, zoo_registry>`. The guide declared by
BOOST_OPENMETHOD then fails to match, and BOOST_OPENMETHOD_OVERRIDE
reported only that it "cannot find 'poke' method that accepts the same
arguments as the overrider", which points nowhere near the cause.

BOOST_OPENMETHOD now declares a second, relaxed guide beside the real
one, `<id>_guide_any_registry`, driven by
`enable_guide_ignoring_registry`: it rewrites every `virtual_ptr`
parameter into the method's own registry, through
`rebind_parameter_registry` and its reference forms, before trying the
call. An overrider that matches that guide and not the strict one
differs from the method in nothing but a registry. The guide never
finds a method to call; it exists only to be asked.

BOOST_OPENMETHOD_DETAIL_LOCATE_METHOD grows a rung under its existing
fallback. The old generic static_assert moves into
`..._explain_method_aux`, whose specialization fires when the relaxed
guide succeeds and inherits `explain_overrider_mismatch`. That pairs
the method's parameters with the overrider's through
`validate_overrider_parameter` - the same instantiation a call through
the thunk would perform - so the diagnosis is "registry mismatch", with
both registries in the trace. The chain is now: strict guide, then the
relaxed guide for the mismatch, then "cannot find".

Diagnostics only: nothing that compiled before stops compiling.

test/compile_fail_overrider_registry_mismatch.cpp covers it. The
compile_fail glob has no CONFIGURE_DEPENDS, so a new file needs a
manual cmake re-run before ctest sees it.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
jll63 added a commit to jll63/openmethod that referenced this pull request Sep 16, 2026
… 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 added a commit to jll63/openmethod that referenced this pull request Sep 16, 2026
… 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
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.

ADL customization point for default value of virtual_ptr's registry parameter

2 participants