Skip to content

Mixed registries: virtual parameters in different registries - #126

Open
jll63 wants to merge 5 commits into
boostorg:developfrom
jll63:feature/mixed-registries
Open

jll63 wants to merge 5 commits into
boostorg:developfrom
jll63:feature/mixed-registries

Conversation

@jll63

@jll63 jll63 commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

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

A method's virtual parameters may now carry different registries, which lets a multi-method dispatch on classes that use different RTTI systems, e.g. a hierarchy with a custom RTTI next to one with standard RTTI:

BOOST_OPENMETHOD(
    render, (virtual_<const Node&, node_registry>, virtual_<const Format&>),
    std::string, default_registry);

Rules

  • Each virtual parameter dispatches in the registry it carries (spelled, or by class affinity), or in the method's when it carries none. Its traits, v-table pointer and type ids come from that registry.
  • Parameters that carry different registries still require the method to name one ("carry conflicting registries"). The named registry may be any registry.
  • The parameter's registry must be initialized before the method's, and the method's again whenever the parameter's is re-initialized. Otherwise the new parameter_registry_not_initialized error is raised: by initialize(), and by a call under runtime_checks. Two registries that each hold a method with a parameter in the other therefore cannot both be initialized.
  • Registries that use deferred_static_rtti mix with those that don't, in either direction: each parameter's type ids are resolved when its own registry resolves them.

How it works

Type ids never cross registries: two registries may give the same id to different classes, or use incomparable ids. The method registers one foreign_parameter_info per foreign parameter with that parameter's registry. That registry's initialize():

  1. maps the ids through its own class map, raising missing_class / missing_base itself;
  2. reserves a slot in its v-tables and leaves the entries empty;
  3. publishes the cone of the parameter's class by position, with each class's entry address.

The method's registry builds proxy classes from the cone and writes the entries in its noexcept commit. The dispatch code is unchanged, and a foreign parameter may be in any position, including the first. A generation counter detects re-initialization.

Module copies share a slot. When modules share a registry, each may hold its own copy of a method, and each registers its own record. The parameter's registry groups them by the address of the method registry's state, the method's type id, and the parameter's position. It cannot compare the method's ids itself - they belong to another rtti policy - so the method template supplies a comparator built from its own registry's rtti::type_index.

Deferral is per parameter. The method's own ids and those of its native parameters follow the method registry's deferral, as before. A foreign parameter whose registry does not defer gets its ids at construction; one whose registry does gets them in that registry's initialize(), through function pointers the templates supply, since that registry knows neither the method's type nor the overriders'. No registry calls another's rtti during static construction.

trace() shows the handshake from both ends. The parameter's registry prints the overrider classes it resolved and what it publishes; the method's registry prints what it received, names the cone classes foreign#k, and lists the entries it writes back into the other registry's v-tables. The addresses match between the two.

Tests and docs

  • test_mixed_registries.cpp uses three registries with different RTTI, two of which deliberately assign the same ids to unrelated classes. It covers a foreign first, second and only parameter, next, the bad-call errors, re-initialization, and (copies_share_the_slot) two module copies of a method sharing one slot.
  • test_mixed_registries_affinity.cpp replaces the five compile-fail tests that expected "registry mismatch", which is now legal.
  • test_mixed_registries_deferred.cpp mixes deferred and non-deferred registries both ways. Its ids are assigned at the start of the test, so an id read during static construction is 0 and dispatch fails.
  • New section Mixing registries in registries_and_policies.adoc, with example mixed_registries.cpp: an expression tree with custom RTTI, rendered through standard-RTTI output formats, and evaluated by a method of the node registry itself. Reference comments are updated.

Verified locally. Headers (0470caf): GCC 13 CMake with -Werror (196/196), GCC 16 with C++26 reflection + shared libs + -Werror (202/202), b2 with -Wall -Wextra -Werror (125 tests), MSVC v18 /W4 /WX on the three mixed-registry tests. Example and docs (a84fc9f): GCC 13 CMake only; the Antora render predates the value section.

Notes

  • Also fixes an ambiguous validate_overrider_parameter specialization when an overrider's const virtual_ptr<C>& parameter is exactly the method's. This predates the change.
  • Interop: virtual_<const std::any&, S> in a method of another registry now dispatches in S (a manual probe works). No test covers it yet, and the other interop headers were not tried. This changes the behaviour described in interop: a registry spelled on an any virtual parameter is never validated against the method's #123: the spelled registry is now honoured rather than ignored.

🤖 Generated with Claude Code

https://claude.ai/code/session_011Eo4q4cYTx457W7oaunqsQ

A method's virtual parameters may now carry different registries, and so
dispatch on classes that use different RTTI systems. Each parameter
dispatches in the registry it carries - its traits, v-table pointer and
type ids all come from there - or in the method's when it carries none.
Parameters that carry different registries still require the method to
name one; it may name any registry.

Type ids never cross registries: two registries may give the same id to
different classes. The method registers a record per foreign parameter
with the parameter's registry, whose initialize() maps the ids, checks the
classes, reserves a slot in its v-tables and publishes the cone of the
parameter's class by position. The method's registry builds its dispatch
data from that and writes the entries at commit. Dispatch code is
unchanged, and parameters may come in any order.

The parameter's registry must therefore be initialized first, and the
method's again after it is re-initialized; a new error,
parameter_registry_not_initialized, is raised by initialize() and, under
runtime checks, by a call. A method and the registries of its parameters
must agree on deferred static rtti.

The five compile-fail tests that expected "registry mismatch" become
test_mixed_registries_affinity.cpp. Also fixes an ambiguous
validate_overrider_parameter specialization when an overrider's
`const virtual_ptr<C>&` parameter is exactly the method's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cppalliance-bot

cppalliance-bot commented Sep 23, 2026 •

Copy link
Copy Markdown

An automated preview of the documentation is available at https://126.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-26 19:19:55 UTC

jll63 and others added 4 commits September 25, 2026 08:20
Three follow-ups to the mixed registries commit.

Module copies of a method now share a slot. Each module's copy registers its
own foreign_parameter_info, and the parameter's registry could not tell them
apart, so it gave each one a slot. It now groups them by the address of the
method registry's state, the method's type_id and the parameter's position.
It cannot compare the method's ids itself - they belong to another rtti
policy - so the method template supplies `same_method`, built from its own
registry's rtti::type_index.

Each parameter's type ids follow the deferral of the registry it dispatches
in, not the method's, so a registry with deferred_static_rtti mixes with one
without, in either direction. resolve_type_ids sets the method's own ids and
the native positions. A foreign position whose registry does not defer is set
at construction; one whose registry does is set by that registry's
initialize(), through `resolve_vp` function pointers the templates supply,
because that registry knows neither the method's type nor the overriders'.
The static_assert that required agreement, and its compile-fail test, are
gone. The slot-sharing key calls `method_type()` rather than reading
method_type_id, which a deferring method registry has not set yet when the
parameter's registry initializes.

The trace follows the handshake across both registries. The parameter's
registry lists each overrider's class for the parameter, says which
parameter an entry it leaves empty belongs to instead of printing "empty",
and ends with "Publishing to methods of other registries". The method's
registry prints what it received per foreign parameter, names the cone
classes foreign#k, and ends with "Entries in the v-tables of other
registries, written at commit" - the step nothing showed before.

init_type_ids gains a Positions argument, hence test_core.cpp. It is marked
[[maybe_unused]] on its `ids` parameter: the pack is empty for a method with
no eager foreign parameter, which -Wextra rejects under b2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Eo4q4cYTx457W7oaunqsQ
`value` is a method of node_registry itself, so the example now shows a
registry hosting its own method beside the slot it reserves for another
registry's parameter - slot 0 and slot 1 in the trace - and the call block
prints each rendering followed by its value.

node_rtti gave every non-Node type the id nullptr. That was harmless with no
methods in node_registry, but two methods would share one key in
augment_methods, their overriders would collide, and the first call would
abort in fn_ambiguous with no message. It now allocates those ids counting
down from the top of the type_id range, where they cannot meet the node ids
1 to 3, and type_name prints them as <1>, <2>... - distinct from the #n the
library's trace uses for indices.

The assertions moved out of the tagged region: the example captures stdout
and checks it afterwards, so the rendered listing shows only the printing.

CLAUDE.md documents the trace, and the per-parameter deferral and slot
sharing of the previous commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Eo4q4cYTx457W7oaunqsQ
`test_mixed_registries_affinity.cpp` wrapped its classes in an anonymous
namespace, so the `boost_openmethod_registry` hidden friend got internal
linkage. It is only ever declared - the affinity machinery reads its return
type and never calls it - and clang rejects that under
-Wunneeded-internal-declaration, which -Wall -Wextra -Werror makes an error:

    error: function 'boost_openmethod_registry' is not needed and will not be
    emitted [-Werror,-Wunneeded-internal-declaration]

Every clang job on the branch failed on it, and no gcc or msvc job did: gcc
has no such warning, and the CMake test flags are -Wall -Werror without
-Wextra, so a local ctest run never saw it.

The classes now sit at namespace scope, as in test_adl_registry_hidden_friend
and the other affinity tests, which is why those never tripped it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Eo4q4cYTx457W7oaunqsQ
The `operator<<` that prints a container of classes uses its `classes`
parameter only inside `if constexpr (Compiler::has_trace)`. MSVC 14.2
(VS2019) counts that as unreferenced and reports C4100, which /W4 /WX turns
into an error:

    initialize.hpp(721): warning C4100: 'classes': unreferenced formal
    parameter

The operator is not new, but nothing instantiated it in a non-tracing build
until the mixed registries commit made `augment_methods` print `method.vp` -
a vector of class_* - where it printed `range{vp_begin, vp_end}` before. The
vector selects this overload; the range selects another.

Newer MSVC does not warn for a parameter used only in a discarded
`if constexpr` branch, so VS2022, VS2026 and the CMake jobs were all green;
only Drone's VS2019 stage caught it, and it had been failing since the first
commit of the branch.

Reproduced locally with the v142 toolset, which VS Build Tools 18 still
ships: `vcvarsall x64_x86 -vcvars_ver=14.29`, /std:c++20 /O2 /W4 /WX. Note
that passing the Boost headers with /external:I and /external:W0 hides this
warning even though it is raised in an openmethod header - use /I to see it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Eo4q4cYTx457W7oaunqsQ
@codecov

codecov Bot commented Sep 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.59649% with 39 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.03%. Comparing base (f093df7) to head (45c404b).
⚠️ Report is 1 commits behind head on develop.

Files with missing lines Patch % Lines
include/boost/openmethod/initialize.hpp 86.03% 14 Missing and 23 partials ⚠️
include/boost/openmethod/core.hpp 97.26% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop     #126      +/-   ##
===========================================
- Coverage    92.34%   92.03%   -0.31%     
===========================================
  Files           27       27              
  Lines         2234     2524     +290     
  Branches       679      801     +122     
===========================================
+ Hits          2063     2323     +260     
- Misses         112      123      +11     
- Partials        59       78      +19     
Files with missing lines Coverage Δ
...oost/openmethod/policies/default_error_handler.hpp 100.00% <ø> (ø)
include/boost/openmethod/preamble.hpp 80.30% <100.00%> (+1.73%) ⬆️
include/boost/openmethod/core.hpp 92.96% <97.26%> (+1.15%) ⬆️
include/boost/openmethod/initialize.hpp 91.95% <86.03%> (-1.03%) ⬇️

... and 2 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 108fd89...45c404b. Read the comment docs.

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

This branch has not been deployed

No deployments
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