From ae8fa4e0b666363212ca2383ea1186230d91444b Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Wed, 23 Sep 2026 08:14:01 -0400 Subject: [PATCH 1/5] mixed registries: virtual parameters in different registries 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&` parameter is exactly the method's. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 83 ++- .../ROOT/examples/mixed_registries.cpp | 123 +++++ .../ROOT/pages/registries_and_policies.adoc | 94 +++- include/boost/openmethod/core.hpp | 373 +++++++++----- include/boost/openmethod/initialize.hpp | 474 +++++++++++++++--- .../policies/default_error_handler.hpp | 5 +- include/boost/openmethod/preamble.hpp | 108 +++- ...le_fail_adl_registry_declared_mismatch.cpp | 38 -- ...compile_fail_adl_registry_ref_mismatch.cpp | 36 -- ...ile_fail_adl_registry_virtual_mismatch.cpp | 34 -- ...compile_fail_mixed_registries_deferred.cpp | 38 ++ ..._fail_virtual_ptr_different_registries.cpp | 29 -- ...compile_fail_virtual_registry_mismatch.cpp | 35 -- test/test_core.cpp | 4 +- test/test_mixed_registries.cpp | 371 ++++++++++++++ test/test_mixed_registries_affinity.cpp | 105 ++++ 16 files changed, 1551 insertions(+), 399 deletions(-) create mode 100644 doc/modules/ROOT/examples/mixed_registries.cpp delete mode 100644 test/compile_fail_adl_registry_declared_mismatch.cpp delete mode 100644 test/compile_fail_adl_registry_ref_mismatch.cpp delete mode 100644 test/compile_fail_adl_registry_virtual_mismatch.cpp create mode 100644 test/compile_fail_mixed_registries_deferred.cpp delete mode 100644 test/compile_fail_virtual_ptr_different_registries.cpp delete mode 100644 test/compile_fail_virtual_registry_mismatch.cpp create mode 100644 test/test_mixed_registries.cpp create mode 100644 test/test_mixed_registries_affinity.cpp diff --git a/CLAUDE.md b/CLAUDE.md index ea80ae0c..615ab4be 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -590,8 +590,10 @@ that is what lets a method mix a class that has an affinity with one that has no `detail::param_registry` says what a parameter carries - **what it carries, never its class's affinity**: a spelled `virtual_ptr` decides the method's registry even though `B` declares nothing. `detail::agreed_registry` folds them, adopters abstaining. A method that names a -registry requires every carrier to carry that one (`validate_method_parameter`, all four shapes); -one that names none takes what the carriers agree on, or the macro default. +registry accepts any carrier - a parameter carrying another registry dispatches in it, see *Mixed +registries* below; one that names none takes what the carriers agree on, or the macro default, and +carriers that disagree are an error there ("carry conflicting registries"), which is what forces +a mixed method to name its registry. **The answer is memoized, so every question carries a `Question` tag.** A class mentioned before it is complete - `virtual_ptr` as a member of `Node`, or through a forward declaration - is @@ -620,11 +622,11 @@ is the strict fold, deliberately *not* `agreed_registry`. Two things declare no affinity - but declaring none is not the same as contributing none, and the interop shapes differ from one another. What each does, with the method on another registry: -| parameter spelling `R` | decides an unannotated method's registry | checked against a method naming another | +| parameter spelling `R` | decides an unannotated method's registry | with a method naming another | |---|---|---| -| `virtual_` (ordinary carrier) | yes | yes - `core.hpp`, "the parameter belongs to another registry" | -| `virtual_` (interop) | yes | **no - nothing checks it** (#123) | -| `virtual_any&` | no, abstains | yes - `virtual_any.hpp`, "registry mismatch" | +| `virtual_` (ordinary carrier) | yes | dispatches in `R` (mixed registries) | +| `virtual_` (interop) | yes | dispatches in `R` - probed with `std::any`, **untested** | +| `virtual_any&` | no, abstains | error - `virtual_any.hpp`, "registry mismatch" | - `interop/std_any.hpp`, `interop/boost_any.hpp`, `interop/boost_type_erasure.hpp` and `interop/virtual_any.hpp` declare no affinity for any class, so a class reached through them @@ -632,9 +634,13 @@ interop shapes differ from one another. What each does, with the method on anoth different thing from declaring an affinity, and the reason #116 gave the `validate_method_parameter` specializations the registry parameter `virtual_` gained in #113. A specialization there must never go back to the bare `virtual_` spelling. Those - specializations accept any `ParamRegistry` without ever comparing it, which is the middle row - above and a bug rather than a decision (#123); `virtual_any`'s are a different shape - (`virtual_any&`) and do compare. None of this is covered in `doc/`. + specializations accept any `ParamRegistry` without comparing it. Before mixed registries that + was a bug (#123): the spelled registry was ignored. Now `parameter_traits` routes the parameter + through `virtual_traits`, so the held types register in `R` and dispatch + there - a mixed `std::any` parameter works in a probe, but no test covers it and the other + interop headers were not tried. `virtual_any`'s specializations are a different shape + (`virtual_any&`) and still require the method's registry. None of this is covered + in `doc/`. - The C++26 `register_classes` still defaults to the macro - its groups may name a namespace, whose classes are only known during the scan that the choice of registry feeds. @@ -650,6 +656,61 @@ carries the override on the file's behalf (`test_capture_errors.hpp` and `test_checked_registry.hpp`). Add another such header and the scan has to learn about it: miss one and the file still compiles, binds to `default_registry`, and fails at run time. +### Mixed registries + +A virtual parameter dispatches in `detail::dispatch_registry` - what it carries, or +the method's registry `R` when it carries nothing - and everything per-parameter goes through that +registry: `parameter_traits`, `method::vptr` (the declared parameter is the template argument, not +`remove_virtual_<>` of it), the type ids (`init_type_ids`, one rtti per +position), `init_bad_call` (one rtti per argument). "Another registry" means another *state*: +`detail::same_registry` compares `registry_type`, so `struct zoo : default_registry {}` is the +default registry under a second name, and a parameter carrying it is native. + +**Type ids never cross registries.** Two registries may give the same id to different classes, or +use rtti policies whose ids are not comparable at all (std_rtti's `type_index` on a `static_rtti` +id is UB). So a foreign parameter is described to the method's registry by *position*, never by id: + +- The method owns one `detail::foreign_parameter_info` per foreign parameter + (`method::foreign_parameters`, a `std::array`), pushed onto + `registry_state_type::foreign_parameters` of the parameter's registry S by the method's + constructor. `method_info::foreign_begin/end` point at them. +- `S::initialize()` (`augment_foreign_parameters`) maps the method's and overriders' ids for that + parameter through its own `class_map` - `missing_class` and `missing_base` are raised there, + by S - gives the parameter a slot like any other (`generic_compiler::parameter::slot` points at + it), leaves the entries empty (`vtbl_entry::method_index == no_method`, written as 0), and + publishes at commit: the slot, the cone of the parameter's class (the class first, then + `classes` order, so every module's copy of the method gets the same layout), each class's entry + address in the new dispatch data, the transitive-derived sets as cone positions, and each + overrider's class as a cone position. +- `R::initialize()` builds proxy `class_`es for the cone (`method::foreign_classes`, empty `ci`, + `is_foreign()`), so `build_dispatch_tables`, `is_more_specific` and the inline-overrider dedup + (which now compares `class_*` vectors, not ids) run unchanged. The entries for the proxies are + collected in `foreign_writes` and written in `commit_global_data`, like everything else. + **`foreign_class_of(method, param, position)` indexes from `foreign_first[param]`, a fixed + base** - an earlier draft indexed from the iterator the loop advanced, and dispatched the wrong + overriders without crashing. +- `registry_state_type::generation` counts S's commits (and `finalize`s). A record remembers the + generation it was published at (`generation`) and the one R built from (`installed_generation`). + R's `initialize` refuses a record that is unpublished or stale; under `runtime_checks`, + `method::check_foreign_parameters` refuses a call once S has been initialized again. Both raise + `parameter_registry_not_initialized`. Consequence: S before R, R again after S, and a cycle + (an R method with an S parameter, an S method with an R parameter) cannot be initialized. +- Each module's copy of a method registers its own records, so S allocates one slot per copy; + R uses the first copy's and propagates `slots_strides` as before. The other slots are wasted, + not wrong. +- `has_deferred_static_rtti` must agree between R and every S (static_assert in `method`, test + `compile_fail_mixed_registries_deferred.cpp`): S resolves the method's deferred ids itself. +- The C++26 scan (`method_traits_aux`) registers only the classes of the parameters that dispatch + in the method's registry (`detail::dispatches_in`). +- `rebind_parameter_registry` still rebinds every `virtual_ptr` to the method's registry, so an + overrider that spells a third registry on a foreign parameter gets "cannot find method" rather + than "registry mismatch". + +Tests: `test_mixed_registries.cpp` (two number-based rtti policies that collide on purpose, plus +std_rtti; foreign first, second and only parameter; `next`; errors; re-initialization) and +`test_mixed_registries_affinity.cpp` (the affinity and spelling shapes that used to be +"registry mismatch" compile-fail tests). + ### Flattened headers for Compiler Explorer `dev/flatten.py` rewrites every public header into a self-sufficient file under `flat/`; the @@ -707,7 +768,9 @@ Registries are completely independent. Use separate registries to: - Apply different policies to different method families - Enable coexistence of incompatible configurations -Registry type must be specified consistently across related methods and classes. +Registry type must be specified consistently across related methods and classes. A method's +virtual parameters may dispatch in other registries, with other rtti policies - see *Mixed +registries*. ## File Organization diff --git a/doc/modules/ROOT/examples/mixed_registries.cpp b/doc/modules/ROOT/examples/mixed_registries.cpp new file mode 100644 index 00000000..c52462ea --- /dev/null +++ b/doc/modules/ROOT/examples/mixed_registries.cpp @@ -0,0 +1,123 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +#include + +#include +#include + +#define BOOST_TEST_MODULE mixed_registries +#include + +using namespace boost::openmethod; + +// tag::nodes[] +struct Node { + explicit Node(unsigned type) : type(type) { + } + + unsigned type; + static constexpr unsigned static_type = 1; +}; + +struct Number : Node { + static constexpr unsigned static_type = 2; + + explicit Number(int value) : Node(static_type), value(value) { + } + + int value; +}; + +struct Plus : Node { + static constexpr unsigned static_type = 3; + + Plus(const Node& left, const Node& right) : + Node(static_type), left(left), right(right) { + } + + const Node& left; + const Node& right; +}; + +struct node_rtti : policies::rtti { + template + struct fn : defaults { + template + static constexpr bool is_polymorphic = std::is_base_of_v; + + template + static auto static_type() -> type_id { + if constexpr (is_polymorphic) { + return reinterpret_cast(T::static_type); + } else { + return nullptr; + } + } + + template + static auto dynamic_type(const T& obj) -> type_id { + if constexpr (is_polymorphic) { + return reinterpret_cast(obj.type); + } else { + return nullptr; + } + } + }; +}; + +struct node_registry : registry {}; + +BOOST_OPENMETHOD_CLASSES(Node, Number, Plus, node_registry); +// end::nodes[] + +// tag::formats[] +struct Format { + virtual ~Format() = default; +}; + +struct Postfix : Format {}; +struct Infix : Format {}; + +BOOST_OPENMETHOD_CLASSES(Format, Postfix, Infix); +// end::formats[] + +// tag::method[] +BOOST_OPENMETHOD( + render, (virtual_, virtual_), + std::string, default_registry); + +BOOST_OPENMETHOD_OVERRIDE( + render, (const Number& number, const Format&), std::string) { + return std::to_string(number.value); +} + +BOOST_OPENMETHOD_OVERRIDE( + render, (const Plus& plus, const Postfix& format), std::string) { + return render(plus.left, format) + " " + render(plus.right, format) + " +"; +} + +BOOST_OPENMETHOD_OVERRIDE( + render, (const Plus& plus, const Infix& format), std::string) { + return "(" + render(plus.left, format) + " + " + + render(plus.right, format) + ")"; +} +// end::method[] + +BOOST_AUTO_TEST_CASE(mixed_registries) { + // tag::initialize[] + initialize(); + initialize(); + // end::initialize[] + + // tag::call[] + Number one(1), two(2), three(3); + Plus sum(one, two), total(sum, three); + + BOOST_TEST(render(total, Postfix()) == "1 2 + 3 +"); + BOOST_TEST(render(total, Infix()) == "((1 + 2) + 3)"); + // end::call[] +} diff --git a/doc/modules/ROOT/pages/registries_and_policies.adoc b/doc/modules/ROOT/pages/registries_and_policies.adoc index 534863a8..555858b1 100644 --- a/doc/modules/ROOT/pages/registries_and_policies.adoc +++ b/doc/modules/ROOT/pages/registries_and_policies.adoc @@ -1,9 +1,11 @@ [#registries_and_policies] -Methods are scoped in a registry. A method can only reference classes in the -same registry. If a class is used as a virtual parameter in methods using -different registries, it must be registered with each of them. +Methods are scoped in a registry. A virtual parameter dispatches in the +method's registry, unless it carries another one - see <> +below - and its classes must be registered in the registry it dispatches in. If +a class is used as a virtual parameter in methods using different registries, +it must be registered with each of them. Class templates cpp:use_classes[], cpp:method[], cpp:virtual_ptr[], and macros xref:reference:BOOST_OPENMETHOD.adoc[BOOST_OPENMETHOD] and @@ -92,10 +94,10 @@ Every class has a registry affinity; a class that declares none has the _default_ affinity, `BOOST_OPENMETHOD_DEFAULT_REGISTRY`. A _declared_ affinity wins over a default one, so a method may mix a class that declares one with a class that does not, and lands in the declared registry. Two virtual parameters -with *different* declared affinities are an error, as is a registry named on the -method that contradicts one of its parameters, whatever its shape. A -`virtual_ptr` parameter contributes the affinity of its class, not the registry -it spells; the two must agree. +with *different* declared affinities do not settle the method's registry +between themselves: the method must name one. A `virtual_ptr` parameter +contributes the affinity of its class, not the registry it spells; the two must +agree. A class can also declare its affinity with a member typedef: @@ -133,8 +135,9 @@ Either can be spelled: `virtual_` and `virtual_ptr` carry `zoo_registry` whatever `Animal` declares. -A method that names a registry requires every parameter that carries one to -carry that one; the parameters that adopt go along. A method that names none +A method that names a registry may name any registry: a parameter that +carries another one dispatches in it (see <>), and the +parameters that adopt go along with the method. A method that names none requires the carriers to agree, and takes their registry - or `BOOST_OPENMETHOD_DEFAULT_REGISTRY` if no parameter carries one. @@ -155,6 +158,79 @@ does not follow affinities: it registers into groups may name a namespace, whose classes are only known during the scan that the choice of registry feeds. +[#mixing_registries] +### Mixing registries + +The virtual parameters of a method may belong to different registries. Each +parameter dispatches in the registry it carries, and that registry identifies +the classes with its own `rtti` policy: this is how a method dispatches on +classes that use different RTTI systems. + +In the following example, the nodes of an expression tree carry their own type +information, and use a custom RTTI policy, as described in +xref:custom_rtti.adoc[Custom RTTI]. They are registered in a registry of their +own: + +[source,c++] +---- +include::example$mixed_registries.cpp[tag=nodes] +---- + +The output formats use the standard RTTI, and are registered in the default +registry: + +[source,c++] +---- +include::example$mixed_registries.cpp[tag=formats] +---- + +`render` dispatches on both. Its first parameter carries `node_registry`, +spelled on it; the second carries nothing, and adopts the method's registry: + +[source,c++] +---- +include::example$mixed_registries.cpp[tag=method] +---- + +The method must name its registry here, even though it is the default one: +without it, the method would take the registry of the only parameter that +carries one, `node_registry`, and `Format` would adopt it. The method's +registry may be any registry - it holds the method and its dispatch table, and +need not be one that its parameters carry. + +A parameter that dispatches in another registry takes a slot in the v-tables of +that registry, which the method's registry fills. The parameter's registry must +therefore be initialized first: + +[source,c++] +---- +include::example$mixed_registries.cpp[tag=initialize] +---- + +Initializing the method's registry first raises a +cpp:parameter_registry_not_initialized[] error. So does calling the method +after the parameter's registry has been initialized again, if the method's +registry has the `runtime_checks` policy: the parameter's registry rebuilds its +v-tables, and the method's registry has to be initialized again as well. +Consequently, two registries cannot each have a method with a parameter that +dispatches in the other: neither can be initialized first. + +Calls work as usual: + +[source,c++] +---- +include::example$mixed_registries.cpp[tag=call] +---- + +The type ids of different registries cannot be compared. Two registries may +give the same id to different classes, or, with different `rtti` policies, ids +of different kinds. The type ids in a cpp:bad_call[] error come each from the +registry of its parameter, so an error handler that prints them must use that +registry's `rtti` policy for each of them. + +A method and the registries that its parameters dispatch in must all use +`deferred_static_rtti`, or none of them. + A registry has a collection of _policies_. Each policy belongs to a policy category. A registry may contain at most one policy of each category. Policies control how type information is obtained, how vptrs are acquired, how errors are diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index e363db1a..6bbd9b0c 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -351,11 +352,14 @@ using class_list_registry = typename pick_class_registry< typename extract_registry::registry, typename extract_registry::others>::type; -template +// Each class's type_id, computed by the rtti policy of the registry in the same +// position: the virtual parameters of a method may belong to different +// registries. +template struct init_type_ids; -template -struct init_type_ids> { +template +struct init_type_ids, mp11::mp_list> { static auto fn(type_id* ids) { (..., (*ids++ = Registry::rtti::template static_type())); @@ -414,6 +418,12 @@ auto optimal_cast(B&& obj) -> decltype(auto) { // ============================================================================= // Common details +// The registry a virtual parameter dispatches in: the one it carries, or the +// method's when it carries none (`void`). +template +using registry_or = + std::conditional_t, Registry, Carried>; + template struct is_virtual : std::false_type {}; @@ -2226,22 +2236,22 @@ struct select_overrider_virtual_type_aux { template struct select_overrider_virtual_type_aux< virtual_, Q, Registry> { - using type = virtual_type; + using type = virtual_type>; }; -template +template struct select_overrider_virtual_type_aux< - virtual_ptr, virtual_ptr, Registry> { + virtual_ptr, virtual_ptr, Registry> { using type = typename virtual_traits< - virtual_ptr, Registry>::virtual_type; + virtual_ptr, ParamRegistry>::virtual_type; }; -template +template struct select_overrider_virtual_type_aux< - const virtual_ptr&, const virtual_ptr&, + const virtual_ptr&, const virtual_ptr&, Registry> { using type = typename virtual_traits< - const virtual_ptr&, Registry>::virtual_type; + const virtual_ptr&, ParamRegistry>::virtual_type; }; template @@ -2256,35 +2266,30 @@ using overrider_virtual_types = boost::mp11::mp_remove< MethodParameters, OverriderParameters>, void>; -template +// `Rtti` identifies the method; each argument is identified by the rtti policy +// in the same position in `ArgRtti`, that of the registry its parameter +// dispatches in. +template struct init_bad_call { - template - static auto fn(bad_call& error, const Arg& arg, const Args&... args) { - if constexpr (Index == 0u) { - error.method = Rtti::template static_type(); - error.arity = sizeof...(args) + 1; - } + template + static auto fn(bad_call& error, const Args&... args) { + error.method = Rtti::template static_type(); + error.arity = sizeof...(args); + std::size_t index = 0; + (..., set_type(error, index++, args)); + } - type_id arg_type_id; + template + static auto set_type(bad_call& error, std::size_t index, const Arg& arg) { + if (index >= bad_call::max_types) { + return; + } if constexpr (is_virtual_ptr) { - arg_type_id = Rtti::dynamic_type(*arg); + error.types[index] = ArgRttiType::dynamic_type(*arg); } else { - arg_type_id = Rtti::dynamic_type(arg); + error.types[index] = ArgRttiType::dynamic_type(arg); } - - error.types[Index] = arg_type_id; - - init_bad_call::fn(error, args...); - } - - static auto fn(bad_call&) { - } -}; - -template -struct init_bad_call { - static auto fn(bad_call&) { } }; @@ -2304,17 +2309,21 @@ struct parameter_traits { } }; +// A virtual parameter is handled by the traits of the registry it carries, +// which is not necessarily the method's. template struct parameter_traits, Registry> : - virtual_traits {}; + virtual_traits> {}; -template -struct parameter_traits, Registry> : - virtual_traits, Registry> {}; +template +struct parameter_traits, Registry> : + virtual_traits, ParamRegistry> {}; -template -struct parameter_traits&, Registry> : - virtual_traits&, Registry> {}; +template +struct parameter_traits< + const virtual_ptr&, Registry> : + virtual_traits< + const virtual_ptr&, ParamRegistry> {}; template struct validate_method_parameter : std::true_type {}; @@ -2325,13 +2334,22 @@ struct validate_method_parameter, Registry, U> : static_assert(false_t, "virtual_traits not specialized for type"); }; +// `ParamRegistry` is what the parameter carries: the registry spelled on it, or +// the one its class declares an affinity for, or `void` when the class +// declares none - in which case the parameter adopts the method's. The +// parameter dispatches in that registry, which need not be the method's, so +// the class is checked against it. template struct validate_method_parameter< virtual_, Registry, - std::void_t::virtual_type>> : + std::void_t>::virtual_type>> : std::bool_constant< - has_vptr_fn, Registry> || - Registry::rtti::template is_polymorphic>> { + has_vptr_fn< + virtual_type>, + registry_or> || + registry_or::rtti::template is_polymorphic< + virtual_type>>> { static_assert( validate_method_parameter::value, "virtual_<> parameter is not a polymorphic class and no " @@ -2339,60 +2357,31 @@ struct validate_method_parameter< // The class is complete here (is_polymorphic needs it), so this is one of // the checkpoints where its affinity is asked again. - static_assert(check_affinity>::value); - - // `ParamRegistry` is what the parameter carries: the registry spelled on - // it, or the one its class declares an affinity for, or `void` when the - // class declares none - in which case the parameter adopts the method's. - // A carrier must agree with the method. - static_assert( - std::is_same_v || - std::is_same_v, - "registry mismatch: the parameter belongs to another registry"); + static_assert(check_affinity< + virtual_type>>::value); }; -// A `virtual_ptr` parameter, in any of its three shapes, must name the -// method's registry. The scan that picks a registry for a method that names -// none takes the class's affinity, so this is where a registry spelled on the -// parameter is held to agree with the class. -template -struct validate_method_parameter, Registry, void> : - std::true_type { - static_assert(check_affinity::value); -}; - -template -struct validate_method_parameter< - virtual_ptr, MethodRegistry, void> : std::false_type { - static_assert( - false_t, "registry mismatch"); -}; - -template +// A `virtual_ptr` parameter, in any of its three shapes, dispatches in the +// registry it names, whether or not it is the method's. The scan that picks a +// registry for a method that names none takes the class's affinity, so this +// is where a registry spelled on the parameter is held to agree with the +// class. +template struct validate_method_parameter< - virtual_ptr&, Registry, void> : std::true_type { + virtual_ptr, Registry, void> : std::true_type { static_assert(check_affinity::value); }; -template +template struct validate_method_parameter< - virtual_ptr&, MethodRegistry, void> : std::false_type { - static_assert( - false_t, "registry mismatch"); -}; - -template -struct validate_method_parameter< - const virtual_ptr&, Registry, void> : std::true_type { + virtual_ptr&, Registry, void> : std::true_type { static_assert(check_affinity::value); }; -template +template struct validate_method_parameter< - const virtual_ptr&, MethodRegistry, void> : - std::false_type { - static_assert( - false_t, "registry mismatch"); + const virtual_ptr&, Registry, void> : std::true_type { + static_assert(check_affinity::value); }; } // namespace detail @@ -2462,6 +2451,28 @@ struct method_registry_aux { template using method_registry = typename method_registry_aux::type; +// The registry a parameter of a method in `Registry` dispatches in: the one it +// carries, or the method's. `Registry` for a non-virtual parameter. +template +using dispatch_registry = + registry_or::type, Registry>; + +// Whether two registries are the same, i.e. share their state. A registry +// derived from another without adding policies is the same registry. +template +using same_registry = std::is_same< + typename Registry::registry_type, typename Other::registry_type>; + +// Whether a parameter of a method in `Registry` dispatches in it. +template +using dispatches_in = + same_registry, Registry>; + +// The class of a virtual parameter of a method in `Registry`. +template +using parameter_class = virtual_type< + remove_virtual_, dispatch_registry>; + } // namespace detail //! Implement a method @@ -2475,17 +2486,28 @@ using method_registry = typename method_registry_aux::type; //! //! `Fn` is a function type, i.e. a type in the form `ReturnType(Parameters...)`. //! -//! `Registry` is an instantiation of class template @ref registry. Methods may -//! use only classes that have been registered in the same registry as virtual -//! parameters and arguments. The registry also contains a set of policies that -//! influence several aspects of the dispatch mechanism - for example, how to -//! acquire a v-table pointer for an object, how to report errors, whether to +//! `Registry` is an instantiation of class template @ref registry. It holds the +//! method and its dispatch data. The registry also contains a set of policies +//! that influence several aspects of the dispatch mechanism - for example, how +//! to acquire a v-table pointer for an object, how to report errors, whether to //! perform sanity checks, etc. //! -//! `Registry` defaults to the registry the virtual parameters of `Fn` have an -//! affinity for - see @ref registry_affinity - and to -//! @ref BOOST_OPENMETHOD_DEFAULT_REGISTRY when none of them declares one. -//! Parameters that declare different registries are an error. +//! Each virtual parameter dispatches in a registry: the one it carries, spelled +//! on it or declared by its class (see @ref registry_affinity), or `Registry` +//! if it carries none. The classes of a virtual parameter, and the arguments +//! passed for it, must be registered in that registry, which identifies them +//! with its own `rtti` policy. This is how a method dispatches on classes +//! that use different RTTI systems. +//! +//! `Registry` defaults to the registry the virtual parameters of `Fn` carry, +//! and to @ref BOOST_OPENMETHOD_DEFAULT_REGISTRY when none of them carries +//! one. Parameters that carry different registries are an error, unless +//! `Registry` is specified; it may then be any registry. +//! +//! A registry in which a parameter of a method of another registry dispatches +//! must be initialized before the method's registry, and the method's registry +//! must be initialized again whenever that registry is. The two must both use +//! @ref policies::deferred_static_rtti, or neither. //! //! Specializations of `method` have a single instance: the static member `fn`, //! which has an `operator()` that forwards to the appropriate overrider. It is @@ -2542,8 +2564,8 @@ using method_registry = typename method_registry_aux::type; //! @tparam Id A type //! @tparam Fn A function type //! @tparam Registry The registry in which the method is defined. Defaults to -//! the registry that `Fn`\'s virtual parameters have an affinity for, and to -//! @ref BOOST_OPENMETHOD_DEFAULT_REGISTRY if none of them has one. +//! the registry that `Fn`\'s virtual parameters carry, and to +//! @ref BOOST_OPENMETHOD_DEFAULT_REGISTRY if none of them carries one. //! //! @see [Core API](xref:ROOT:core_api.adoc) template> @@ -2586,6 +2608,12 @@ class method : boost::mp11::mp_transform; using VirtualParameters = typename detail::virtual_types; + // The registry each virtual parameter dispatches in, and its class. + using VirtualRegistries = mp11::mp_transform_q< + mp11::mp_bind_back, + mp11::mp_filter>; + using VirtualClasses = mp11::mp_transform< + detail::virtual_type, VirtualParameters, VirtualRegistries>; using Signature = auto(Parameters...) -> ReturnType; using FunctionPointer = auto (*)(detail::remove_virtual_...) -> ReturnType; @@ -2724,7 +2752,29 @@ class method : detail::validate_method_parameter::value && ...)); static_assert(Arity > 0, "method has no virtual parameters"); + // The virtual parameters that dispatch in another registry, by position. + template + using is_foreign_parameter = mp11::mp_not< + detail::same_registry, Registry>>; + using ForeignParameters = + mp11::mp_copy_if, is_foreign_parameter>; + static constexpr auto ForeignCount = + mp11::mp_size::value; + + // A foreign parameter's type ids are resolved by its registry's + // initialize(), which runs before the method's: both must defer, or + // neither. + template + using same_deferral = mp11::mp_bool< + ParamRegistry::has_deferred_static_rtti == + Registry::has_deferred_static_rtti>; + static_assert( + mp11::mp_all_of::value, + "a method and the registries of its virtual parameters must all use " + "deferred static rtti, or none of them"); + type_id vp_type_ids[Arity]; + std::array foreign_parameters; std::size_t slots_strides[2 * Arity - 1]; // Slots followed by strides. No stride for first virtual argument. @@ -2737,9 +2787,11 @@ class method : void resolve_type_ids(); - template + template auto vptr(const ArgType& arg) const -> vptr_type; + void check_foreign_parameters() const; + template auto resolve_uni(const ArgType& arg, const MoreArgTypes&... more_args) const -> detail::word; @@ -2855,10 +2907,23 @@ method::method() { this->vp_end = vp_type_ids + Arity; this->not_implemented = reinterpret_cast(fn_not_implemented); this->ambiguous = reinterpret_cast(fn_ambiguous); + this->foreign_begin = foreign_parameters.data(); + this->foreign_end = foreign_parameters.data() + ForeignCount; // zero-initalized static variable // coverity[uninit_use] Registry::static_::st.methods.push_back(*this); + + // Each foreign parameter goes to the registry it dispatches in. + mp11::mp_for_each>([this](auto index) { + using Param = mp11::mp_at; + using ParamRegistry = mp11::mp_at; + auto& param = foreign_parameters[index]; + param.method = this; + param.param = Param::value; + param.host_generation = &ParamRegistry::static_::st.generation; + ParamRegistry::static_::st.foreign_parameters.push_back(param); + }); } template< @@ -2868,16 +2933,19 @@ void method::resolve_type_ids() { this->method_type_id = rtti::template static_type(); this->return_type_id = rtti::template static_type>(); - init_type_ids< - Registry, - mp11::mp_transform_q< - mp11::mp_bind_back, - VirtualParameters>>::fn(this->vp_type_ids); + init_type_ids::fn(this->vp_type_ids); } template< typename Id, typename... Parameters, typename ReturnType, class Registry> method::~method() { + mp11::mp_for_each>([this](auto index) { + using Param = mp11::mp_at; + using ParamRegistry = mp11::mp_at; + ParamRegistry::static_::st.foreign_parameters.remove( + foreign_parameters[index]); + }); + Registry::static_::st.methods.remove(*this); } @@ -2909,6 +2977,10 @@ BOOST_FORCEINLINE Registry::require_initialized(); + if constexpr (Registry::has_runtime_checks && ForeignCount > 0) { + check_foreign_parameters(); + } + void (*pf)(); if constexpr (Arity == 1) { @@ -2922,24 +2994,51 @@ BOOST_FORCEINLINE return reinterpret_cast(pf); } +// A foreign parameter's entries live in the v-tables of its registry, which +// clears them when it is initialized again: then the method's registry must be +// initialized again too. +template< + typename Id, typename... Parameters, typename ReturnType, class Registry> +void method::check_foreign_parameters() + const { + for (auto& param : foreign_parameters) { + if (param.installed_generation != *param.host_generation) { + if constexpr (Registry::has_error_handler) { + parameter_registry_not_initialized error; + error.method = this->method_type_id; + error.param = param.param; + Registry::error_handler::error(error); + } + + abort(); + } + } +} + +// The v-table pointer comes from the registry the parameter dispatches in. template< typename Id, typename... Parameters, typename ReturnType, class Registry> -template +template BOOST_FORCEINLINE auto method::vptr( const ArgType& arg) const -> vptr_type { - if constexpr (detail::is_virtual_ptr) { + using namespace detail; + + if constexpr (is_virtual_ptr) { return arg.vptr(); } else { - decltype(auto) obj = virtual_traits::peek(arg); - - if constexpr (detail::has_vptr_fn) { - return boost_openmethod_vptr(obj, static_cast(nullptr)); - } else if constexpr ( - detail::has_vptr< - virtual_traits, decltype(obj)>) { - return virtual_traits::vptr(obj); + using ParamRegistry = dispatch_registry; + using Traits = + virtual_traits, ParamRegistry>; + decltype(auto) obj = Traits::peek(arg); + + if constexpr (has_vptr_fn) { + return boost_openmethod_vptr( + obj, static_cast(nullptr)); + } else if constexpr (has_vptr) { + return Traits::vptr(obj); } else { - return Registry::template policy::dynamic_vptr(obj); + return ParamRegistry::template policy::dynamic_vptr( + obj); } } } @@ -2957,7 +3056,7 @@ method::resolve_uni( using namespace boost::mp11; if constexpr (is_virtual>::value) { - vptr_type vtbl = vptr>>(arg); + vptr_type vtbl = vptr>(arg); return vtbl[this->slots_strides[0]]; } else { @@ -2977,7 +3076,7 @@ method::resolve_multi_first( using namespace boost::mp11; if constexpr (is_virtual>::value) { - vptr_type vtbl = vptr>>(arg); + vptr_type vtbl = vptr>(arg); std::size_t slot = this->slots_strides[0]; // The first virtual parameter is special. Since its stride is @@ -3008,7 +3107,7 @@ method::resolve_multi_next( using namespace boost::mp11; if constexpr (is_virtual>::value) { - vptr_type vtbl = vptr>>(arg); + vptr_type vtbl = vptr>(arg); std::size_t slot = this->slots_strides[VirtualArg]; std::size_t stride = this->slots_strides[Arity + VirtualArg - 1]; dispatch = dispatch + vtbl[slot].i * stride; @@ -3047,13 +3146,15 @@ template< BOOST_NORETURN auto method::fn_not_implemented( detail::remove_virtual_... args) -> ReturnType { + using namespace detail; using namespace policies; if constexpr (Registry::has_error_handler) { no_overrider error; - detail::init_bad_call::fn( - error, - detail::parameter_traits::peek(args)...); + init_bad_call< + method, rtti, + typename dispatch_registry::rtti...>:: + fn(error, parameter_traits::peek(args)...); Registry::error_handler::error(error); } @@ -3065,13 +3166,15 @@ template< BOOST_NORETURN auto method::fn_ambiguous( detail::remove_virtual_... args) -> ReturnType { + using namespace detail; using namespace policies; if constexpr (Registry::has_error_handler) { ambiguous_call error; - detail::init_bad_call::fn( - error, - detail::parameter_traits::peek(args)...); + init_bad_call< + method, rtti, + typename dispatch_registry::rtti...>:: + fn(error, parameter_traits::peek(args)...); Registry::error_handler::error(error); } @@ -3127,10 +3230,22 @@ struct validate_overrider_parameter, virtual_, void> : static_assert(false_t, "virtual_<> is not allowed in overriders"); }; +// The same `virtual_ptr` in the method and the overrider matches both the +// generic `` and the shape-specific specializations below; these are +// more specialized than either. template struct validate_overrider_parameter< virtual_ptr, virtual_ptr, void> : std::true_type {}; +template +struct validate_overrider_parameter< + const virtual_ptr&, const virtual_ptr&, void> : + std::true_type {}; + +template +struct validate_overrider_parameter< + virtual_ptr&&, virtual_ptr&&, void> : std::true_type {}; + template struct validate_overrider_parameter< virtual_ptr, virtual_ptr, void> : std::true_type { @@ -3331,7 +3446,7 @@ void method::override_impl< this->type = Registry::rtti::template static_type(); using Thunk = thunk; detail::init_type_ids< - Registry, typename Thunk::OverriderVirtualParameters>::fn(this + typename Thunk::OverriderVirtualParameters, VirtualRegistries>::fn(this ->vp_type_ids); } @@ -3352,10 +3467,14 @@ struct method_traits_aux> { // return type is registered when the scan finds it deriving from one of // these, or when it is listed - as in C++17 - not as a root of its own. // A root return type would make `initialize` demand a registration for - // the return type of every overrider, `std::ostringstream` included. + // the return type of every overrider, `std::ostringstream` included. Nor + // the classes of the parameters that dispatch in another registry: they + // are registered there. using type = mp11::mp_transform_q< - mp11::mp_bind_back, - virtual_types>>; + mp11::mp_bind_back, + mp11::mp_filter_q< + mp11::mp_bind_back, + mp11::mp_filter>>>; }; // Read from reflection, by `substitute`-ing a method into it and taking the diff --git a/include/boost/openmethod/initialize.hpp b/include/boost/openmethod/initialize.hpp index 73aa6d64..4a1d62ce 100644 --- a/include/boost/openmethod/initialize.hpp +++ b/include/boost/openmethod/initialize.hpp @@ -263,13 +263,22 @@ struct generic_compiler { struct method; + // A virtual parameter that takes a slot in the v-tables of this registry. struct parameter { + // Where the slot goes. + std::size_t* slot; + // The method, or null if it is in another registry. struct method* method; std::size_t param; }; + static constexpr auto no_method = (std::numeric_limits::max)(); + struct vtbl_entry { - std::size_t method_index, vp_index, group_index; + // `no_method` for an entry that this registry does not write: a hole, + // or the slot of a parameter of another registry's method, which that + // registry writes. + std::size_t method_index = no_method, vp_index = 0, group_index = 0; }; struct class_ { @@ -301,11 +310,43 @@ struct generic_compiler { return transitive_derived.find(other) != transitive_derived.end(); } + // A class of another registry, in the cone of a foreign parameter, + // has no class_info; it is known by its entry in that registry's + // v-table (see foreign_class). + detail::word* foreign_entry = nullptr; + bool foreign_abstract = false; + + auto is_foreign() const -> bool { + return ci.empty(); + } + auto is_abstract() const -> bool { - return ci[0]->is_abstract; + return is_foreign() ? foreign_abstract : ci[0]->is_abstract; } }; + // A parameter of a method of another registry, dispatched through the + // v-tables of this one. + struct foreign_parameter { + detail::foreign_parameter_info* info; + class_* vp; + // The class of each overrider, for this parameter. + std::vector overriders; + std::size_t slot = 0; + // Staged by write_global_data(), published by commit_global_data(). + std::vector cone; + std::vector overrider_positions; + }; + + // An entry in the v-table of another registry, filled for one of this + // registry's methods. + struct foreign_write { + detail::word* entry; + struct method* method; + std::size_t dim, group; + detail::word value; + }; + struct slot_choice { std::size_t slot, cost; }; @@ -361,6 +402,12 @@ struct generic_compiler { overrider not_implemented; overrider ambiguous; vptr_type gv_dispatch_table = nullptr; + // For each virtual parameter, the position of its proxies in + // `foreign_classes` if it dispatches in another registry, else + // `no_method`. + std::vector foreign_first; + // Proxies for the classes of the foreign parameters' cones. + std::deque foreign_classes; auto arity() const { return vp.size(); } @@ -514,7 +561,30 @@ struct generic_compiler { return const_class_iterator(classes.end(), classes.end()); } + // The record of a method's virtual parameter, if it dispatches in another + // registry. + static auto foreign_parameter_of( + const detail::method_info& info, std::size_t param) + -> const detail::foreign_parameter_info* { + for (auto iter = info.foreign_begin; iter != info.foreign_end; ++iter) { + if (iter->param == param) { + return iter; + } + } + + return nullptr; + } + + // The proxy for the class at `position` in the cone of a foreign + // parameter. + static auto foreign_class_of( + method& m, std::size_t param, std::size_t position) -> class_* { + return &m.foreign_classes[m.foreign_first[param] + position]; + } + std::deque methods; + std::deque foreign_parameters; + std::vector foreign_writes; std::size_t class_mark = 0; bool compilation_done = false; }; @@ -625,7 +695,11 @@ template auto operator<<(trace_stream& tr, const generic_compiler::class_& cls) -> trace_stream& { if constexpr (Compiler::has_trace) { - tr << type_name(cls.ci[0]->type); + if (cls.is_foreign()) { + tr << "(class of another registry)"; + } else { + tr << type_name(cls.ci[0]->type); + } } return tr; @@ -770,6 +844,10 @@ auto operator<<(trace_stream& tr, const range& range) -> auto& { return write_range(tr, range, [](auto value) { return value; }); } +struct parameter_name { + const generic_compiler::parameter& param; +}; + template auto operator<<(trace_stream& tr, const type_name& manip) -> auto& { if constexpr (Compiler::has_trace) { @@ -778,6 +856,24 @@ auto operator<<(trace_stream& tr, const type_name& manip) -> auto& { return tr; } + +// The method of a parameter of another registry's method is not named: its +// type_id is meaningful only to that registry's rtti policy. +template +auto operator<<(trace_stream& tr, const parameter_name& manip) + -> auto& { + if constexpr (Compiler::has_trace) { + if (manip.param.method) { + tr << type_name(manip.param.method->infos[0]->method_type_id); + } else { + tr << "(method of another registry)"; + } + + tr << " parameter " << manip.param.param; + } + + return tr; +} } // namespace detail // Definition of the nested template struct outside the registry class @@ -803,6 +899,7 @@ struct registry::compiler : detail::generic_compiler { void calculate_transitive_bases(class_& cls); void calculate_transitive_derived(class_& cls); void augment_methods(); + void augment_foreign_parameters(); void assign_slots(); void assign_tree_slots(class_& cls, std::size_t base_slot); void assign_slots(class_& cls); @@ -1204,12 +1301,33 @@ void registry::compiler::augment_methods() { (void)info; // unused when BOOST_ASSERT compiles out (NDEBUG) BOOST_ASSERT(info->arity() == first_info->arity()); } - method.vp.reserve(first_info->arity()); - method.slots.resize(first_info->arity()); + auto arity = first_info->arity(); + method.vp.reserve(arity); + method.slots.resize(arity); + method.foreign_first.assign(arity, no_method); - ++tr << type_name(first_info->method_type_id) << " " - << range{first_info->vp_begin, first_info->vp_end} << "\n"; - indent _(tr); + // A parameter that dispatches in another registry was prepared by that + // registry's initialize(), which must have run since every module's + // copy of the method registered it. + for (auto info : method.infos) { + for (auto& param : range{info->foreign_begin, info->foreign_end}) { + if (param.generation == 0 || + param.generation != *param.host_generation || + param.overriders.size() != info->overriders.size()) { + ++tr << "registry of parameter #" << param.param + << " not initialized\n"; + + if constexpr (has_error_handler) { + parameter_registry_not_initialized error; + error.method = info->method_type_id; + error.param = param.param; + error_handler::error(error); + } + + abort(); + } + } + } // Build the virtual-parameter classes once, from the local // method_info. All copies describe the same method, so any one of them @@ -1218,6 +1336,39 @@ void registry::compiler::augment_methods() { std::size_t param_index = 0; for (auto ti : range{first_info->vp_begin, first_info->vp_end}) { + if (auto param = + foreign_parameter_of(*first_info, param_index)) { + // The type_id means nothing here: the parameter's registry + // described the cone of its class, by position. + method.slots[param_index] = param->slot; + method.foreign_first[param_index] = + method.foreign_classes.size(); + + for (auto& cls : param->cone) { + auto& proxy = method.foreign_classes.emplace_back(); + proxy.foreign_entry = cls.entry; + proxy.foreign_abstract = cls.is_abstract; + } + + std::size_t position = 0; + + for (auto& cls : param->cone) { + auto proxy = + foreign_class_of(method, param_index, position++); + + for (auto derived : cls.transitive_derived) { + proxy->transitive_derived.insert( + foreign_class_of(method, param_index, derived)); + } + } + + method.vp.push_back( + foreign_class_of(method, param_index, 0)); + ++param_index; + + continue; + } + auto class_ = class_map[rtti::type_index(ti)]; if (!class_) { ++tr << "unknown class " << ti << "(" << type_name(ti) @@ -1237,6 +1388,10 @@ void registry::compiler::augment_methods() { } } + ++tr << type_name(first_info->method_type_id) << " " << method.vp + << "\n"; + indent _(tr); + if (rtti::type_index(first_info->return_type_id) != rtti::type_index(rtti::template static_type())) { auto covariant_return_iter = @@ -1273,42 +1428,69 @@ void registry::compiler::augment_methods() { // method<...>::override and method<...>::override, both // non-inline by default) are always genuinely distinct and must // remain ambiguous. - std::vector all_specs; + // + // Overriders are compared by the classes of their virtual parameters, + // not by type_id: the type_id of a parameter that dispatches in + // another registry means nothing to this one. + struct candidate { + detail::overrider_info* info; + std::vector vp; + }; + + std::vector all_specs; std::size_t module_index = 0; for (auto info : method.infos) { indent _(tr); ++tr << "module " << module_index++ << "\n"; + std::size_t spec_position = 0; for (auto& spec : info->overriders) { - auto same = [&](const detail::overrider_info* kept) { - if (!kept->inline_ || !spec.inline_) { - return false; - } + std::vector vp; + vp.reserve(arity); + std::size_t param_index = 0; + + for (auto type : range{spec.vp_begin, spec.vp_end}) { + if (auto param = foreign_parameter_of(*info, param_index)) { + vp.push_back(foreign_class_of( + method, param_index, + param->overriders[spec_position])); + ++param_index; - if (rtti::type_index(kept->type) != - rtti::type_index(spec.type)) { - return false; + continue; } - // Same function type id: confirm the virtual-parameter - // signatures match too, so two genuinely different - // overriders that happen to share a function type id are - // never merged. - auto a = kept->vp_begin, ae = kept->vp_end, - b = spec.vp_begin; + auto class_ = class_map[rtti::type_index(type)]; + + if (!class_) { + indent _(tr); + ++tr << "unknown class error for *virtual* parameter #" + << (param_index + 1) << "\n"; + missing_class error; + error.type = type; - for (; a != ae; ++a, ++b) { - if (rtti::type_index(*a) != rtti::type_index(*b)) { - return false; + if constexpr (has_error_handler) { + error_handler::error(error); } + + abort(); } - return true; + vp.push_back(class_); + ++param_index; + } + + ++spec_position; + + auto same = [&](const candidate& kept) { + return kept.info->inline_ && spec.inline_ && + rtti::type_index(kept.info->type) == + rtti::type_index(spec.type) && + kept.vp == vp; }; if (std::none_of(all_specs.begin(), all_specs.end(), same)) { - all_specs.push_back(&spec); + all_specs.push_back({&spec, std::move(vp)}); } } } @@ -1326,43 +1508,16 @@ void registry::compiler::augment_methods() { method.overriders.resize(spec_size); auto spec_iter = method.overriders.begin(); - for (auto overrider_info : all_specs) { - if constexpr (has_deferred_static_rtti) { - static_cast(*overrider_info) - .resolve_type_ids(); - } - + for (auto& spec : all_specs) { + auto overrider_info = spec.info; spec_iter->method_index = method.index; spec_iter->spec_index = spec_iter - method.overriders.begin(); ++tr << type_name(overrider_info->type) << " (" << overrider_info->pf << ")\n"; spec_iter->info = overrider_info; - spec_iter->vp.reserve(first_info->arity()); - std::size_t param_index = 0; - - for ( - auto type : - range{overrider_info->vp_begin, overrider_info->vp_end}) { - indent _(tr); - auto class_ = class_map[rtti::type_index(type)]; - - if (!class_) { - ++tr << "unknown class error for *virtual* parameter #" - << (param_index + 1) << "\n"; - missing_class error; - error.type = type; - - if constexpr (has_error_handler) { - error_handler::error(error); - } - - abort(); - } - - spec_iter->pf = spec_iter->info->pf; - spec_iter->vp.push_back(class_); - } + spec_iter->pf = overrider_info->pf; + spec_iter->vp = std::move(spec.vp); if (method.covariant_return_type) { auto covariant_return_iter = class_map.find( @@ -1391,6 +1546,14 @@ void registry::compiler::augment_methods() { std::size_t param_index = 0; for (auto vp : method.vp) { + // The parameter's registry checked the classes of a foreign + // parameter, and allocated its slot. + if (vp->is_foreign()) { + ++param_index; + + continue; + } + for (auto& overrider : method.overriders) { if (overrider.vp[param_index] == vp) { continue; @@ -1409,8 +1572,84 @@ void registry::compiler::augment_methods() { } } - vp->used_by_vp.push_back({&method, param_index++}); + vp->used_by_vp.push_back( + {&method.slots[param_index], &method, param_index}); + ++param_index; + } + } + + augment_foreign_parameters(); +} + +// The parameters of other registries' methods that dispatch through this +// registry's v-tables. Their type_ids were computed by this registry's rtti +// policy, so this is where they are mapped to classes and checked. Each gets a +// slot like a parameter of this registry's own methods, but the entries are +// left empty: the method's registry fills them. +template +template +void registry::compiler::augment_foreign_parameters() { + using namespace detail; + + if (registry::static_::st.foreign_parameters.empty()) { + return; + } + + ++tr << "Parameters of methods of other registries:\n"; + indent _(tr); + + auto find_class = [this](type_id type) { + auto iter = class_map.find(rtti::type_index(type)); + + if (iter == class_map.end()) { + ++tr << "unknown class " << type_name(type) << "\n"; + missing_class error; + error.type = type; + + if constexpr (has_error_handler) { + error_handler::error(error); + } + + abort(); + } + + return iter->second; + }; + + for (auto& info : registry::static_::st.foreign_parameters) { + // Both registries defer, or neither (see `method`). + if constexpr (has_deferred_static_rtti) { + static_cast(*info.method).resolve_type_ids(); + + for (auto& spec : info.method->overriders) { + static_cast(spec).resolve_type_ids(); + } + } + + auto& param = foreign_parameters.emplace_back(); + param.info = &info; + param.vp = find_class(info.method->vp_begin[info.param]); + ++tr << "parameter " << info.param << ": " << *param.vp << "\n"; + + for (auto& spec : info.method->overriders) { + auto cls = find_class(spec.vp_begin[info.param]); + + if (cls != param.vp && !param.vp->is_base_of(cls)) { + missing_base error; + error.base = param.vp->ci[0]->type; + error.derived = cls->ci[0]->type; + + if constexpr (has_error_handler) { + error_handler::error(error); + } + + abort(); + } + + param.overriders.push_back(cls); } + + param.vp->used_by_vp.push_back({¶m.slot, nullptr, info.param}); } } @@ -1519,10 +1758,9 @@ void registry::compiler::assign_tree_slots( auto next_slot = base_slot; for (const auto& mp : cls.used_by_vp) { - ++tr << "in " << cls << " for " - << type_name(mp.method->infos[0]->method_type_id) << " parameter " - << mp.param << ": slot " << next_slot << "\n"; - mp.method->slots[mp.param] = next_slot++; + ++tr << "in " << cls << " for " << parameter_name{mp} << ": slot " + << next_slot << "\n"; + *mp.slot = next_slot++; } // `used_slots` stays empty: the v-table is sized here, and nothing in a @@ -1557,11 +1795,10 @@ void registry::compiler::assign_slots(class_& cls) { for (const auto& mp : cls.used_by_vp) { auto choice = pick_slot(cls.transitive_derived, unavailable); - ++tr << "in " << cls << " for " - << type_name(mp.method->infos[0]->method_type_id) << " parameter " - << mp.param << ": slot " << choice.slot << ", cost " << choice.cost - << ", unavailable " << unavailable << "\n"; - mp.method->slots[mp.param] = choice.slot; + ++tr << "in " << cls << " for " << parameter_name{mp} << ": slot " + << choice.slot << ", cost " << choice.cost << ", unavailable " + << unavailable << "\n"; + *mp.slot = choice.slot; set_bit(unavailable, choice.slot); // One bit, to the whole cone: `cls` is in it. @@ -1649,7 +1886,15 @@ void registry::compiler::build_dispatch_tables() { for (auto cls : group.classes) { indent _(tr); - ++tr << type_name(cls->ci[0]->type) << "\n"; + ++tr << *cls << "\n"; + + if (cls->is_foreign()) { + // Written at commit, see write_global_data(). + foreign_writes.push_back( + {cls->foreign_entry, &m, dim, group_num, {}}); + continue; + } + auto& entry = cls->vtbl[m.slots[dim] - cls->first_slot]; entry.method_index = m.index; entry.vp_index = dim; @@ -1718,7 +1963,7 @@ void registry::compiler::build_dispatch_table( << "\n"; indent _(tr); for (auto cls : range{group.classes.begin(), group.classes.end()}) { - ++tr << type_name(cls->ci[0]->type) << "\n"; + ++tr << *cls << "\n"; } } @@ -1948,6 +2193,14 @@ void registry::compiler::write_global_data() { indent _(tr); for (auto& entry : cls.vtbl) { + if (entry.method_index == no_method) { + ++tr << "empty\n"; + BOOST_ASSERT(gv_iter + 1 <= gv_last); + *gv_iter++ = std::size_t(0); + + continue; + } + ++tr << "method #" << entry.method_index << " "; auto& method = methods[entry.method_index]; @@ -1980,6 +2233,55 @@ void registry::compiler::write_global_data() { ++tr << rflush(4, dispatch_data_size) << " " << gv_iter << " end\n"; + // The entries of this registry's methods in other registries' v-tables. + // They are written at commit, like everything else. + for (auto& write : foreign_writes) { + auto& m = *write.method; + + if (m.arity() == 1) { + write.value = m.dispatch_table[write.group]->pf; + } else if (write.dim == 0) { + write.value = std::uintptr_t(m.gv_dispatch_table + write.group); + } else { + write.value = write.group; + } + } + + // What this registry publishes for other registries' parameters: the + // cone of the parameter's class - the class first, then the others in + // `classes` order, which is the same for every module's copy of the + // method - with each class's entry in the new v-tables. The dispatch data + // is swapped into place at commit, which keeps the addresses. + for (auto& param : foreign_parameters) { + std::vector cone{param.vp}; + std::unordered_map position{{param.vp, 0}}; + + for (auto& cls : classes) { + if (&cls != param.vp && param.vp->is_base_of(&cls)) { + position.emplace(&cls, cone.size()); + cone.push_back(&cls); + } + } + + param.cone.reserve(cone.size()); + + for (auto cls : cone) { + auto& foreign = param.cone.emplace_back(); + // The dispatch data is ours; `vptr` is const only for the users of + // the v-tables. + foreign.entry = const_cast(cls->vptr) + param.slot; + foreign.is_abstract = cls->is_abstract(); + + for (auto derived : cls->transitive_derived) { + foreign.transitive_derived.push_back(position.at(derived)); + } + } + + for (auto cls : param.overriders) { + param.overrider_positions.push_back(position.at(cls)); + } + } + detail::registry_state_transaction< registry, compiler, std::tuple> transaction; @@ -2045,6 +2347,32 @@ void registry::compiler::commit_global_data( } new_dispatch_data.swap(static_::st.dispatch_data); + + // After the swap: the entries below are in the new dispatch data. A + // method of another registry dispatching through the old one learns that + // its entries are gone from the generation. + auto generation = ++static_::st.generation; + + for (auto& param : foreign_parameters) { + param.info->slot = param.slot; + param.info->cone.swap(param.cone); + param.info->overriders.swap(param.overrider_positions); + param.info->generation = generation; + } + + for (auto& write : foreign_writes) { + *write.entry = write.value; + } + + for (auto& m : methods) { + for (auto info : m.infos) { + for ( + auto& param : + detail::range{info->foreign_begin, info->foreign_end}) { + param.installed_generation = param.generation; + } + } + } } template @@ -2248,7 +2576,7 @@ void registry::compiler::print_slots() { for (auto cls : lattice) { for (const auto& mp : cls->used_by_vp) { - slots.push_back({mp.method->slots[mp.param], cls, mp}); + slots.push_back({*mp.slot, cls, mp}); } } @@ -2263,8 +2591,7 @@ void registry::compiler::print_slots() { for (const auto& e : slots) { ++tr << e.slot << ": in " << *e.cls << " for " - << type_name(e.mp.method->infos[0]->method_type_id) - << " parameter " << e.mp.param << "\n"; + << parameter_name{e.mp} << "\n"; } } @@ -2420,6 +2747,7 @@ auto registry::finalize(Options... opts) -> void { std::tuple options(opts...); detail::finalize_policies::fn(options); static_::st.dispatch_data.clear(); + ++static_::st.generation; static_::st.initialized = false; } diff --git a/include/boost/openmethod/policies/default_error_handler.hpp b/include/boost/openmethod/policies/default_error_handler.hpp index b9706e0c..5cc87461 100644 --- a/include/boost/openmethod/policies/default_error_handler.hpp +++ b/include/boost/openmethod/policies/default_error_handler.hpp @@ -78,8 +78,9 @@ struct default_error_handler : error_handler { using error_variant = typename error_variant_aux< void, std::variant< - not_initialized, no_overrider, ambiguous_call, missing_class, - missing_base, odr_violation, final_error>, + not_initialized, parameter_registry_not_initialized, + no_overrider, ambiguous_call, missing_class, missing_base, + odr_violation, final_error>, typename Registry::policy_list>::type; //! The type of the error handler function object. diff --git a/include/boost/openmethod/preamble.hpp b/include/boost/openmethod/preamble.hpp index 8528ee82..5d11a741 100644 --- a/include/boost/openmethod/preamble.hpp +++ b/include/boost/openmethod/preamble.hpp @@ -178,6 +178,34 @@ struct not_initialized : openmethod_error { } }; +//! Registry of a virtual parameter not initialized +//! +//! A method's virtual parameter carries a registry other than the method's, and +//! that registry was not initialized before the method's, or was initialized +//! again since. The parameter's registry allocates the parameter's slot in its +//! v-tables, and the method's registry fills it, so the parameter's registry +//! must be initialized first, and the method's again whenever the parameter's +//! is. +//! +//! The error is raised by the method's registry: by @ref initialize, and, if +//! runtime checks are enabled, by a call to the method. +//! +//! @see [Error Handling](xref:ROOT:error_handling.adoc) +struct parameter_registry_not_initialized : openmethod_error { + //! The type_id of the method, in the method's registry. + type_id method; + //! The position of the parameter among the method's virtual parameters, + //! starting from zero. + std::size_t param; + + //! Write a short description to an output stream + //! @param os The output stream + //! @tparam Registry The registry + //! @tparam Stream A @ref LightweightOutputStream + template + auto write(Stream& os) const; +}; + //! Missing class. //! //! A class used as a virtual parameter in a method, an overrider or a method @@ -273,7 +301,9 @@ struct bad_call : openmethod_error { std::size_t arity; //! The maximum size of `types` static constexpr std::size_t max_types = 16; - //! The type_ids of the arguments. + //! The type_ids of the arguments, each obtained from the `rtti` policy of + //! the registry its parameter dispatches in. They may come from different + //! registries, see @ref method. type_id types[max_types]; }; @@ -362,8 +392,12 @@ struct deferred_class_info : class_info { // method info struct overrider_info; +struct foreign_parameter_info; struct method_info : static_list::static_link { + // The type ids of the virtual parameters, each computed by the rtti policy + // of the parameter's registry - which is not the method's for a foreign + // parameter, see foreign_parameter_info. Likewise in overrider_info. type_id* vp_begin; type_id* vp_end; static_list overriders; @@ -372,12 +406,60 @@ struct method_info : static_list::static_link { type_id method_type_id; type_id return_type_id; std::size_t* slots_strides_ptr; + foreign_parameter_info* foreign_begin; + foreign_parameter_info* foreign_end; auto arity() const { return std::distance(vp_begin, vp_end); } }; +// A class in the cone of a foreign parameter, as the parameter's registry +// describes it to the method's. Classes are identified by their position in +// the cone, never by type_id: the two registries may not share an rtti policy, +// and the same type_id can then name different classes in each. +struct foreign_class { + // Where the class's v-table holds the parameter's entry. The parameter's + // registry owns the memory and leaves the entry empty; the method's + // registry writes it. + word* entry; + bool is_abstract; + // Positions in the cone of the class and of every class deriving from it. + std::vector transitive_derived; +}; + +// A virtual parameter whose registry is not the method's. The method owns one +// per such parameter, and registers it with the parameter's registry. That +// registry's initialize() maps the type_ids - which only its rtti policy can +// interpret - allocates the slot in its v-tables, and publishes the cone of +// the parameter's class. The method's registry's initialize() builds the +// dispatch data from that, and writes the entries. +struct foreign_parameter_info : + static_list::static_link { + method_info* method; + // The position of the parameter among the method's virtual parameters. + std::size_t param; + // The initialization count of the parameter's registry. + const std::size_t* host_generation; + + // Published by the parameter's registry. + + // The value of `*host_generation` when these were published, 0 if never. + std::size_t generation; + std::size_t slot; + // The cone of the parameter's class; the class itself comes first. A + // registry lays it out in the same order for every copy of the method, + // one per module. + std::vector cone; + // For each overrider of `method`, in list order, the position of its class + // for this parameter in `cone`. + std::vector overriders; + + // Written by the method's registry: the `generation` its dispatch data was + // built from. + std::size_t installed_generation; +}; + struct deferred_method_info : method_info { virtual void resolve_type_ids() = 0; }; @@ -1003,7 +1085,13 @@ template struct registry_state_type { static_list classes; static_list methods; + // The parameters of other registries' methods that dispatch through this + // registry's v-tables. + static_list foreign_parameters; bool initialized; + // Incremented each time the dispatch data is replaced or released, so that + // a method in another registry can tell that its entries are gone. + std::size_t generation; std::vector dispatch_data; // The per-policy `state` objects are held in a detail::tuple, whose // element types must be unique (each is a distinct base class). If two @@ -1170,16 +1258,19 @@ detail::registry_state_type registry_state::st; //! Methods, classes and policies. //! -//! Methods exist in the context of a registry. Any class used as a method or -//! overrider parameter, or in as a method call argument, must be registered -//! with the same registry. +//! Methods exist in the context of a registry. Any class used as a virtual +//! parameter of a method or an overrider, or passed as a virtual argument, must +//! be registered with the registry the parameter dispatches in: the method's, +//! unless the parameter carries another one (see @ref method). //! //! Before calling a method, its registry must be initialized with the @ref //! initialize function. This is typically done at the beginning of `main`. //! //! Multiple registries can co-exist in the same program. They must be //! initialized individually. Classes referenced by methods in different -//! registries must be registered with each registry. +//! registries must be registered with each registry. A registry in which a +//! parameter of another registry's method dispatches must be initialized +//! before that registry. //! //! A registry also contains a set of @ref policies that control how certain //! operations are performed. For example, the `rtti` policy provides type @@ -1417,6 +1508,13 @@ auto missing_class::write(Stream& os) const { Registry::rtti::type_name(type, os); } +template +auto parameter_registry_not_initialized::write(Stream& os) const { + os << "registry of virtual parameter #" << param << " of "; + Registry::rtti::type_name(method, os); + os << " not initialized, or initialized again since"; +} + template auto missing_base::write(Stream& os) const { os << "missing base "; diff --git a/test/compile_fail_adl_registry_declared_mismatch.cpp b/test/compile_fail_adl_registry_declared_mismatch.cpp deleted file mode 100644 index 511da69f..00000000 --- a/test/compile_fail_adl_registry_declared_mismatch.cpp +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) 2017-2026 Jean-Louis Leroy -// Distributed under the Boost Software License, Version 1.0. -// See accompanying file LICENSE_1_0.txt -// or copy at http://www.boost.org/LICENSE_1_0.txt) - -// Expected diagnostic, as a CMake regex (see CMakeLists.txt). -// expected-error: registry mismatch - -#include - -#include - -using namespace boost::openmethod; - -struct zoo_registry : default_registry {}; -struct other_registry : default_registry {}; - -struct Animal { - virtual ~Animal() = default; - friend auto boost_openmethod_registry(Animal*) -> zoo_registry; -}; - -// `virtual_ptr` carries `zoo_registry`, because that is what `Animal` -// says. Declaring the method in another registry contradicts the parameter - -// the check that already guarded an explicitly spelled `virtual_ptr`. -BOOST_OPENMETHOD(poke, (virtual_ptr), std::string, other_registry); - -int main() { - // The check is a fold in `method`'s class body, so it needs the class to be - // instantiated. Declaring the method is not enough: gcc and clang - // instantiate it anyway through the registrar, but MSVC does not, and the - // file then compiles. Calling it forces the point, on every compiler - like - // compile_fail_virtual_ptr_different_registries.cpp does. - Animal animal; - poke(animal); - - return 0; -} diff --git a/test/compile_fail_adl_registry_ref_mismatch.cpp b/test/compile_fail_adl_registry_ref_mismatch.cpp deleted file mode 100644 index 98432d20..00000000 --- a/test/compile_fail_adl_registry_ref_mismatch.cpp +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) 2017-2026 Jean-Louis Leroy -// Distributed under the Boost Software License, Version 1.0. -// See accompanying file LICENSE_1_0.txt -// or copy at http://www.boost.org/LICENSE_1_0.txt) - -// Expected diagnostic, as a CMake regex (see CMakeLists.txt). -// expected-error: registry mismatch - -#include - -#include - -using namespace boost::openmethod; - -struct zoo_registry : default_registry {}; -struct other_registry : default_registry {}; - -struct Animal { - virtual ~Animal() = default; - friend auto boost_openmethod_registry(Animal*) -> zoo_registry; -}; - -// The contradiction of compile_fail_adl_registry_declared_mismatch.cpp, with -// the `virtual_ptr` passed by reference. `virtual_ptr` carries -// `zoo_registry` in every shape. -BOOST_OPENMETHOD( - poke, (const virtual_ptr&), std::string, other_registry); - -int main() { - // See compile_fail_adl_registry_declared_mismatch.cpp for why the call. - Animal animal; - virtual_ptr vp(animal); - poke(vp); - - return 0; -} diff --git a/test/compile_fail_adl_registry_virtual_mismatch.cpp b/test/compile_fail_adl_registry_virtual_mismatch.cpp deleted file mode 100644 index e33796b9..00000000 --- a/test/compile_fail_adl_registry_virtual_mismatch.cpp +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2017-2026 Jean-Louis Leroy -// Distributed under the Boost Software License, Version 1.0. -// See accompanying file LICENSE_1_0.txt -// or copy at http://www.boost.org/LICENSE_1_0.txt) - -// Expected diagnostic, as a CMake regex (see CMakeLists.txt). -// expected-error: registry mismatch - -#include - -#include - -using namespace boost::openmethod; - -struct zoo_registry : default_registry {}; -struct other_registry : default_registry {}; - -struct Animal { - virtual ~Animal() = default; - friend auto boost_openmethod_registry(Animal*) -> zoo_registry; -}; - -// Same contradiction as compile_fail_adl_registry_declared_mismatch.cpp, in -// the `virtual_<>` shape: the class says `zoo_registry`, the method says -// otherwise. -BOOST_OPENMETHOD(poke, (virtual_), std::string, other_registry); - -int main() { - // See compile_fail_adl_registry_declared_mismatch.cpp for why the call. - Animal animal; - poke(animal); - - return 0; -} diff --git a/test/compile_fail_mixed_registries_deferred.cpp b/test/compile_fail_mixed_registries_deferred.cpp new file mode 100644 index 00000000..abaae137 --- /dev/null +++ b/test/compile_fail_mixed_registries_deferred.cpp @@ -0,0 +1,38 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +// Expected diagnostic, as a CMake regex (see CMakeLists.txt). +// expected-error: deferred static rtti + +#include + +using namespace boost::openmethod; + +// Standard RTTI, with the type ids resolved at initialize() time. +struct deferred_std_rtti : policies::deferred_static_rtti { + template + struct fn : policies::std_rtti::fn {}; +}; + +struct deferred_registry : default_registry::with {}; + +struct Animal { + virtual ~Animal() = default; +}; + +// The parameter's type ids are resolved by its registry, which defers, while +// the method's registry does not: the ids would be read before they exist. +BOOST_OPENMETHOD( + poke, (virtual_), void, default_registry); + +int main() { + // The check is in `method`'s class body. gcc and clang instantiate it + // through the registrar, but MSVC does not: calling the method forces the + // point on every compiler. + Animal animal; + poke(animal); + + return 0; +} diff --git a/test/compile_fail_virtual_ptr_different_registries.cpp b/test/compile_fail_virtual_ptr_different_registries.cpp deleted file mode 100644 index 2d48110f..00000000 --- a/test/compile_fail_virtual_ptr_different_registries.cpp +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2017-2026 Jean-Louis Leroy -// Distributed under the Boost Software License, Version 1.0. -// See accompanying file LICENSE_1_0.txt -// or copy at http://www.boost.org/LICENSE_1_0.txt) - -// Expected diagnostic, as a CMake regex (see CMakeLists.txt). -// expected-error: registry mismatch - -#include - -using namespace boost::openmethod; - -struct Cat { - virtual ~Cat() { - } -}; - -struct other_registry : - default_registry::without::with< - policies::runtime_checks> {}; - -BOOST_OPENMETHOD(poke, (virtual_ptr), void, other_registry); - -int main() { - Cat felix; - poke(felix); - - return 0; -} diff --git a/test/compile_fail_virtual_registry_mismatch.cpp b/test/compile_fail_virtual_registry_mismatch.cpp deleted file mode 100644 index 6205bc28..00000000 --- a/test/compile_fail_virtual_registry_mismatch.cpp +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) 2017-2026 Jean-Louis Leroy -// Distributed under the Boost Software License, Version 1.0. -// See accompanying file LICENSE_1_0.txt -// or copy at http://www.boost.org/LICENSE_1_0.txt) - -// Expected diagnostic, as a CMake regex (see CMakeLists.txt). -// expected-error: registry mismatch - -#include - -#include - -using namespace boost::openmethod; - -struct zoo_registry : default_registry {}; - -struct Animal { - virtual ~Animal() = default; -}; - -// The parameter carries zoo_registry - spelled on it, since Animal declares -// nothing - and the method says another. A parameter that carries a registry -// must agree with its method; only one that adopts, `virtual_`, -// would go along. -BOOST_OPENMETHOD( - speak, (virtual_), std::string, - default_registry); - -int main() { - // See compile_fail_adl_registry_declared_mismatch.cpp for why the call. - Animal animal; - speak(animal); - - return 0; -} diff --git a/test/test_core.cpp b/test/test_core.cpp index 0a639bb4..c8d7bb70 100644 --- a/test/test_core.cpp +++ b/test/test_core.cpp @@ -135,7 +135,9 @@ static_assert(!detail::is_registry); BOOST_AUTO_TEST_CASE(test_init_type_ids) { type_id ids[2]; - auto last = init_type_ids>::fn(ids); + auto last = init_type_ids< + mp11::mp_list, + mp11::mp_list>::fn(ids); BOOST_TEST_REQUIRE(last - ids == 2); BOOST_TEST_REQUIRE(ids[0] == type_id(&typeid(a))); BOOST_TEST_REQUIRE(ids[1] == type_id(&typeid(b))); diff --git a/test/test_mixed_registries.cpp b/test/test_mixed_registries.cpp new file mode 100644 index 00000000..4154178a --- /dev/null +++ b/test/test_mixed_registries.cpp @@ -0,0 +1,371 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +// Methods whose virtual parameters belong to different registries, each with +// its own rtti policy. `zoo_registry` and `garage_registry` identify classes +// by a number stored in the object, and give the same numbers to unrelated +// classes: a registry that looked up another's type_ids in its own tables +// would find the wrong class. `space_registry` uses standard RTTI. + +#include +#include +#include + +#include +#include +#include + +#define BOOST_TEST_MODULE mixed_registries +#include + +using namespace boost::openmethod; + +namespace { + +auto next_non_polymorphic_id() -> std::size_t { + static std::size_t counter = 0; + + return (std::size_t(1) << (sizeof(std::size_t) * 8 - 1)) | ++counter; +} + +template +auto non_polymorphic_static_type() -> std::size_t { + static std::size_t value = next_non_polymorphic_id(); + + return value; +} + +// Classes derived from `Root` carry their type number in `type`. +template +struct number_rtti : policies::rtti { + template + struct fn : defaults { + template + static constexpr bool is_polymorphic = std::is_base_of_v; + + template + static auto static_type() -> type_id { + if constexpr (is_polymorphic) { + return type_id(T::static_type); + } else { + return type_id(non_polymorphic_static_type()); + } + } + + template + static auto dynamic_type(const T& obj) -> type_id { + if constexpr (is_polymorphic) { + return type_id(obj.type); + } else { + return type_id(non_polymorphic_static_type()); + } + } + + template + static void type_name(type_id type, Stream& stream) { + auto index = reinterpret_cast(type); + stream << (index >= 1 && index <= 3 ? Root::names[index - 1] : "?"); + } + + static auto type_index(type_id type) -> type_id { + return type; + } + }; +}; + +struct Animal { + static constexpr std::size_t static_type = 1; + static constexpr const char* names[] = {"Animal", "Dog", "Cat"}; + std::size_t type; + + explicit Animal(std::size_t type = static_type) : type(type) { + } +}; + +struct Dog : Animal { + static constexpr std::size_t static_type = 2; + + Dog() : Animal(static_type) { + } +}; + +struct Cat : Animal { + static constexpr std::size_t static_type = 3; + + Cat() : Animal(static_type) { + } +}; + +// Same type numbers as the animals. +struct Vehicle { + static constexpr std::size_t static_type = 1; + static constexpr const char* names[] = {"Vehicle", "Car", "Truck"}; + std::size_t type; + + explicit Vehicle(std::size_t type = static_type) : type(type) { + } +}; + +struct Car : Vehicle { + static constexpr std::size_t static_type = 2; + + Car() : Vehicle(static_type) { + } +}; + +struct Truck : Vehicle { + static constexpr std::size_t static_type = 3; + + Truck() : Vehicle(static_type) { + } +}; + +struct Planet { + virtual ~Planet() = default; +}; + +struct Earth : Planet {}; +struct Mars : Planet {}; + +struct zoo_registry : + default_registry::with>::without { +}; + +// Checked, because it holds methods with parameters in `zoo_registry`. +struct garage_registry : + default_registry::with>::without:: + with {}; + +struct space_registry : + default_registry::with {}; + +// Holds methods only; every parameter is foreign. +struct arbiter_registry : + default_registry::with< + policies::runtime_checks, policies::throw_error_handler> {}; + +BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat, zoo_registry); +BOOST_OPENMETHOD_CLASSES(Vehicle, Car, Truck, garage_registry); +BOOST_OPENMETHOD_CLASSES(Planet, Earth, Mars, space_registry); + +// ----------------------------------------------------------------------------- +// A method of the registry of its parameter, sharing the v-tables with the +// foreign parameters below. + +BOOST_OPENMETHOD(kind, (virtual_), std::string, zoo_registry); + +BOOST_OPENMETHOD_OVERRIDE(kind, (const Animal&), std::string) { + return "animal"; +} + +BOOST_OPENMETHOD_OVERRIDE(kind, (const Dog&), std::string) { + return "dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(kind, (const Cat&), std::string) { + return "cat"; +} + +// ----------------------------------------------------------------------------- +// Foreign first parameter: its entry, a pointer into the method's dispatch +// table, is written in the v-tables of `zoo_registry`. + +BOOST_OPENMETHOD( + run_over, (virtual_, virtual_), + std::string, garage_registry); + +BOOST_OPENMETHOD_OVERRIDE( + run_over, (const Animal&, const Vehicle&), std::string) { + return "animal, vehicle"; +} + +BOOST_OPENMETHOD_OVERRIDE(run_over, (const Dog&, const Car&), std::string) { + return "dog, car"; +} + +BOOST_OPENMETHOD_OVERRIDE(run_over, (const Cat&, const Truck&), std::string) { + return "cat, truck"; +} + +// Two equally good overriders for (Dog, Truck). +BOOST_OPENMETHOD_OVERRIDE( + run_over, (const Dog& dog, const Vehicle& vehicle), std::string) { + return "dog, vehicle -> " + next(dog, vehicle); +} + +BOOST_OPENMETHOD_OVERRIDE( + run_over, (const Animal&, const Truck&), std::string) { + return "animal, truck"; +} + +// ----------------------------------------------------------------------------- +// Foreign second parameter. + +BOOST_OPENMETHOD( + carry, (virtual_, virtual_), + std::string, garage_registry); + +BOOST_OPENMETHOD_OVERRIDE(carry, (const Car&, const Dog&), std::string) { + return "car, dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(carry, (const Truck&, const Animal&), std::string) { + return "truck, animal"; +} + +// ----------------------------------------------------------------------------- +// A uni-method on a foreign parameter, in a registry that has no classes. + +BOOST_OPENMETHOD( + horn, (virtual_), std::string, + arbiter_registry); + +BOOST_OPENMETHOD_OVERRIDE(horn, (const Car&), std::string) { + return "beep"; +} + +BOOST_OPENMETHOD_OVERRIDE(horn, (const Truck&), std::string) { + return "honk"; +} + +// ----------------------------------------------------------------------------- +// Three registries, three rtti policies, and the `virtual_ptr` shapes. + +BOOST_OPENMETHOD( + land, + (virtual_ptr, + const virtual_ptr&, + virtual_), + std::string, arbiter_registry); + +BOOST_OPENMETHOD_OVERRIDE( + land, + (virtual_ptr, + const virtual_ptr&, const Planet&), + std::string) { + return "animal, vehicle, planet"; +} + +BOOST_OPENMETHOD_OVERRIDE( + land, + (virtual_ptr, + const virtual_ptr&, const Earth&), + std::string) { + return "dog, car, earth"; +} + +BOOST_OPENMETHOD_OVERRIDE( + land, + (virtual_ptr, + const virtual_ptr&, const Mars&), + std::string) { + return "cat, truck, mars"; +} + +template +auto throws(Fn fn) -> bool { + try { + fn(); + } catch (const Error&) { + return true; + } + + return false; +} + +void initialize_all() { + initialize(trace::from_env()); + initialize(trace::from_env()); + initialize(trace::from_env()); + initialize(trace::from_env()); +} + +} // namespace + +BOOST_AUTO_TEST_CASE(parameter_registry_first) { + // `garage_registry` holds methods with a parameter in `zoo_registry`, + // which has not been initialized yet. + BOOST_TEST(throws([] { + initialize(); + })); + + initialize_all(); +} + +BOOST_AUTO_TEST_CASE(dispatch) { + initialize_all(); + + Animal animal; + Dog dog; + Cat cat; + Vehicle vehicle; + Car car; + Truck truck; + Earth earth; + Mars mars; + + BOOST_TEST(kind(animal) == "animal"); + BOOST_TEST(kind(dog) == "dog"); + BOOST_TEST(kind(cat) == "cat"); + + BOOST_TEST(run_over(animal, vehicle) == "animal, vehicle"); + BOOST_TEST(run_over(dog, car) == "dog, car"); + BOOST_TEST(run_over(cat, truck) == "cat, truck"); + BOOST_TEST(run_over(cat, car) == "animal, vehicle"); + BOOST_TEST(run_over(dog, vehicle) == "dog, vehicle -> animal, vehicle"); + BOOST_TEST(run_over(animal, truck) == "animal, truck"); + BOOST_TEST(throws([&] { run_over(dog, truck); })); + + BOOST_TEST(carry(car, dog) == "car, dog"); + BOOST_TEST(carry(truck, cat) == "truck, animal"); + BOOST_TEST(throws([&] { carry(car, cat); })); + BOOST_TEST(throws([&] { carry(vehicle, dog); })); + + BOOST_TEST(horn(car) == "beep"); + BOOST_TEST(horn(truck) == "honk"); + BOOST_TEST(throws([&] { horn(vehicle); })); + + BOOST_TEST( + land( + virtual_ptr(dog), + virtual_ptr(car), + earth) == "dog, car, earth"); + BOOST_TEST( + land( + virtual_ptr(cat), + virtual_ptr(truck), + mars) == "cat, truck, mars"); + BOOST_TEST( + land( + virtual_ptr(dog), + virtual_ptr(car), + mars) == "animal, vehicle, planet"); +} + +BOOST_AUTO_TEST_CASE(parameter_registry_initialized_again) { + initialize_all(); + + Dog dog; + Car car; + + BOOST_TEST(run_over(dog, car) == "dog, car"); + + // Clears the entries of `garage_registry`'s methods in the v-tables of + // `zoo_registry`. + initialize(); + BOOST_TEST(kind(dog) == "dog"); + BOOST_TEST(throws([&] { + run_over(dog, car); + })); + BOOST_TEST( + throws([&] { carry(car, dog); })); + + // Methods on parameters of other registries are not affected. + BOOST_TEST(horn(car) == "beep"); + + initialize(); + BOOST_TEST(run_over(dog, car) == "dog, car"); + BOOST_TEST(carry(car, dog) == "car, dog"); +} diff --git a/test/test_mixed_registries_affinity.cpp b/test/test_mixed_registries_affinity.cpp new file mode 100644 index 00000000..ddc3b3bb --- /dev/null +++ b/test/test_mixed_registries_affinity.cpp @@ -0,0 +1,105 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +// A method may be declared in any registry, whatever registries its parameters +// carry - through a class affinity, or spelled on the parameter. A parameter +// that carries another registry dispatches in it. + +#include +#include +#include +#include + +#include +#include + +#define BOOST_TEST_MODULE mixed_registries_affinity +#include + +using namespace boost::openmethod; + +namespace { + +struct zoo_registry : default_registry::with> {}; + +// Holds the methods. +struct other_registry : + default_registry::with {}; + +struct Animal { + virtual ~Animal() = default; + friend auto boost_openmethod_registry(Animal*) -> zoo_registry; +}; + +struct Dog : Animal {}; + +// No affinity. +struct Vehicle { + virtual ~Vehicle() = default; +}; + +struct Car : Vehicle {}; + +BOOST_OPENMETHOD_CLASSES(Animal, Dog); +BOOST_OPENMETHOD_CLASSES(Vehicle, Car, zoo_registry); +BOOST_OPENMETHOD_CLASSES(Vehicle, Car, other_registry); + +static_assert(std::is_same_v, zoo_registry>); + +// `virtual_ptr` carries `zoo_registry`, by affinity. +BOOST_OPENMETHOD(by_value, (virtual_ptr), std::string, other_registry); + +BOOST_OPENMETHOD_OVERRIDE(by_value, (virtual_ptr), std::string) { + return "dog"; +} + +BOOST_OPENMETHOD( + by_ref, (const virtual_ptr&), std::string, other_registry); + +BOOST_OPENMETHOD_OVERRIDE(by_ref, (const virtual_ptr&), std::string) { + return "dog"; +} + +// `virtual_` carries it too, by affinity. +BOOST_OPENMETHOD( + by_virtual, (virtual_), std::string, other_registry); + +BOOST_OPENMETHOD_OVERRIDE(by_virtual, (const Dog&), std::string) { + return "dog"; +} + +// `Vehicle` declares nothing: spelled, it goes to `zoo_registry`; otherwise it +// adopts the method's registry. +BOOST_OPENMETHOD( + spelled, (virtual_), std::string, + other_registry); + +BOOST_OPENMETHOD_OVERRIDE(spelled, (const Car&), std::string) { + return "car"; +} + +BOOST_OPENMETHOD( + adopted, (virtual_, virtual_), std::string, + other_registry); + +BOOST_OPENMETHOD_OVERRIDE(adopted, (const Dog&, const Car&), std::string) { + return "dog, car"; +} + +} // namespace + +BOOST_AUTO_TEST_CASE(any_method_registry) { + initialize(); + initialize(); + + Dog dog; + Car car; + + BOOST_TEST(by_value(virtual_ptr(dog)) == "dog"); + BOOST_TEST(by_ref(virtual_ptr(dog)) == "dog"); + BOOST_TEST(by_virtual(dog) == "dog"); + BOOST_TEST(spelled(car) == "car"); + BOOST_TEST(adopted(dog, car) == "dog, car"); +} From 0470caf734492afc215edcd4b1288e5b083f5c2f Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Fri, 25 Sep 2026 08:20:10 -0400 Subject: [PATCH 2/5] mixed registries: shared slots, per-parameter deferral, traced handshake 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 Claude-Session: https://claude.ai/code/session_011Eo4q4cYTx457W7oaunqsQ --- include/boost/openmethod/core.hpp | 118 ++++++--- include/boost/openmethod/initialize.hpp | 224 +++++++++++++++--- include/boost/openmethod/preamble.hpp | 18 ++ ...compile_fail_mixed_registries_deferred.cpp | 38 --- test/test_core.cpp | 6 +- test/test_mixed_registries.cpp | 55 +++++ test/test_mixed_registries_deferred.cpp | 211 +++++++++++++++++ 7 files changed, 570 insertions(+), 100 deletions(-) delete mode 100644 test/compile_fail_mixed_registries_deferred.cpp create mode 100644 test/test_mixed_registries_deferred.cpp diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index 6bbd9b0c..d673a23a 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -352,21 +352,35 @@ using class_list_registry = typename pick_class_registry< typename extract_registry::registry, typename extract_registry::others>::type; -// Each class's type_id, computed by the rtti policy of the registry in the same +// Set `ids[Index]`, for each `Index` in `Positions`, to the type_id of the class +// in that position, computed by the rtti policy of the registry in the same // position: the virtual parameters of a method may belong to different -// registries. -template +// registries, and their type ids may be resolved at different times. +template struct init_type_ids; -template -struct init_type_ids, mp11::mp_list> { - static auto fn(type_id* ids) { - (..., (*ids++ = Registry::rtti::template static_type())); - - return ids; +template +struct init_type_ids> { + // `Positions` is empty for a method whose virtual parameters all belong to + // other registries, and are all resolved elsewhere. + static void fn([[maybe_unused]] type_id* ids) { + (..., + (ids[Index::value] = + mp11::mp_at::rtti::template static_type< + mp11::mp_at>())); } }; +// The same, for one position, known at run time. +template +void init_type_id(type_id* ids, std::size_t position) { + mp11::mp_with_index::value>( + position, [ids](auto index) { + ids[index] = mp11::mp_at::rtti:: + template static_type>(); + }); +} + template struct is_unambiguous_accessible_base_of : std::is_base_of { static_assert( @@ -2506,8 +2520,9 @@ using parameter_class = virtual_type< //! //! A registry in which a parameter of a method of another registry dispatches //! must be initialized before the method's registry, and the method's registry -//! must be initialized again whenever that registry is. The two must both use -//! @ref policies::deferred_static_rtti, or neither. +//! must be initialized again whenever that registry is. Each parameter's type +//! ids follow the registry it dispatches in: a registry with +//! @ref policies::deferred_static_rtti may be mixed with one without. //! //! Specializations of `method` have a single instance: the static member `fn`, //! which has an `operator()` that forwards to the appropriate overrider. It is @@ -2761,17 +2776,21 @@ class method : static constexpr auto ForeignCount = mp11::mp_size::value; - // A foreign parameter's type ids are resolved by its registry's - // initialize(), which runs before the method's: both must defer, or - // neither. - template - using same_deferral = mp11::mp_bool< - ParamRegistry::has_deferred_static_rtti == - Registry::has_deferred_static_rtti>; - static_assert( - mp11::mp_all_of::value, - "a method and the registries of its virtual parameters must all use " - "deferred static rtti, or none of them"); + // A parameter's type ids follow the deferral of the registry it dispatches + // in. Those of the parameters in the method's registry are resolved with + // the method's own, by `resolve_type_ids`; those of a foreign parameter, + // at construction, or - if its registry defers them - by that registry's + // initialize(), through `resolve_type_id`. + template + using is_deferred_parameter = mp11::mp_bool< + mp11::mp_at::has_deferred_static_rtti>; + using NativeParameters = + mp11::mp_remove_if, is_foreign_parameter>; + using EagerForeignParameters = + mp11::mp_remove_if; + + static auto method_type() -> type_id; + static void resolve_type_id(type_id* ids, std::size_t param); type_id vp_type_ids[Arity]; std::array foreign_parameters; @@ -2847,6 +2866,7 @@ class method : detail::overrider_info> { explicit override_impl(FunctionPointer* next = nullptr); void resolve_type_ids(); + static void resolve_type_id(type_id* ids, std::size_t param); static type_id vp_type_ids[Arity]; }; @@ -2903,6 +2923,10 @@ method::method() { resolve_type_ids(); } + detail::init_type_ids< + VirtualClasses, VirtualRegistries, + EagerForeignParameters>::fn(vp_type_ids); + this->vp_begin = vp_type_ids; this->vp_end = vp_type_ids + Arity; this->not_implemented = reinterpret_cast(fn_not_implemented); @@ -2922,6 +2946,13 @@ method::method() { param.method = this; param.param = Param::value; param.host_generation = &ParamRegistry::static_::st.generation; + param.method_state = &Registry::static_::st; + param.same_method = [](type_id a, type_id b) { + return Registry::rtti::type_index(a) == + Registry::rtti::type_index(b); + }; + param.method_type = method_type; + param.resolve_vp = resolve_type_id; ParamRegistry::static_::st.foreign_parameters.push_back(param); }); } @@ -2930,10 +2961,24 @@ template< typename Id, typename... Parameters, typename ReturnType, class Registry> void method::resolve_type_ids() { using namespace detail; - this->method_type_id = rtti::template static_type(); + this->method_type_id = method_type(); this->return_type_id = rtti::template static_type>(); - init_type_ids::fn(this->vp_type_ids); + init_type_ids::fn( + this->vp_type_ids); +} + +template< + typename Id, typename... Parameters, typename ReturnType, class Registry> +auto method::method_type() -> type_id { + return rtti::template static_type(); +} + +template< + typename Id, typename... Parameters, typename ReturnType, class Registry> +void method::resolve_type_id( + type_id* ids, std::size_t param) { + detail::init_type_id(ids, param); } template< @@ -3422,10 +3467,15 @@ method::override_impl< resolve_type_ids(); } + using Thunk = thunk; + init_type_ids< + typename Thunk::OverriderVirtualParameters, VirtualRegistries, + EagerForeignParameters>::fn(vp_type_ids); + this->resolve_vp = resolve_type_id; + this->next = reinterpret_cast( p_next ? p_next : &method::next); - using Thunk = thunk; this->pf = reinterpret_cast(Thunk::fn); this->vp_begin = vp_type_ids; @@ -3445,9 +3495,21 @@ void method::override_impl< virtual_type>(); this->type = Registry::rtti::template static_type(); using Thunk = thunk; - detail::init_type_ids< - typename Thunk::OverriderVirtualParameters, VirtualRegistries>::fn(this - ->vp_type_ids); + init_type_ids< + typename Thunk::OverriderVirtualParameters, VirtualRegistries, + NativeParameters>::fn(vp_type_ids); +} + +template< + typename Id, typename... Parameters, typename ReturnType, class Registry> +template +void method:: + override_impl::resolve_type_id( + type_id* ids, std::size_t param) { + using Thunk = thunk; + detail::init_type_id< + typename Thunk::OverriderVirtualParameters, VirtualRegistries>( + ids, param); } // ============================================================================= diff --git a/include/boost/openmethod/initialize.hpp b/include/boost/openmethod/initialize.hpp index 4a1d62ce..895b2ab4 100644 --- a/include/boost/openmethod/initialize.hpp +++ b/include/boost/openmethod/initialize.hpp @@ -315,6 +315,8 @@ struct generic_compiler { // v-table (see foreign_class). detail::word* foreign_entry = nullptr; bool foreign_abstract = false; + // The position of the class in the cone, for the trace. + std::size_t foreign_position = 0; auto is_foreign() const -> bool { return ci.empty(); @@ -326,22 +328,30 @@ struct generic_compiler { }; // A parameter of a method of another registry, dispatched through the - // v-tables of this one. + // v-tables of this one. Each module may register a copy of the method, + // and with it a record of the parameter; the copies share the slot. struct foreign_parameter { - detail::foreign_parameter_info* info; + struct copy { + detail::foreign_parameter_info* info; + // The class of each overrider of this copy, for the parameter. + std::vector overriders; + // Staged by write_global_data(), published by + // commit_global_data(). + std::vector cone; + std::vector overrider_positions; + }; + class_* vp; - // The class of each overrider, for this parameter. - std::vector overriders; + std::vector copies; std::size_t slot = 0; - // Staged by write_global_data(), published by commit_global_data(). - std::vector cone; - std::vector overrider_positions; }; // An entry in the v-table of another registry, filled for one of this // registry's methods. struct foreign_write { detail::word* entry; + // The class, in the trace. + const class_* cls; struct method* method; std::size_t dim, group; detail::word value; @@ -696,7 +706,7 @@ auto operator<<(trace_stream& tr, const generic_compiler::class_& cls) -> trace_stream& { if constexpr (Compiler::has_trace) { if (cls.is_foreign()) { - tr << "(class of another registry)"; + tr << "foreign#" << cls.foreign_position; } else { tr << type_name(cls.ci[0]->type); } @@ -900,6 +910,9 @@ struct registry::compiler : detail::generic_compiler { void calculate_transitive_derived(class_& cls); void augment_methods(); void augment_foreign_parameters(); + void trace_foreign_parameters(const detail::method_info& info); + void trace_unfilled_entry( + class_& cls, std::size_t slot, const detail::word* address); void assign_slots(); void assign_tree_slots(class_& cls, std::size_t base_slot); void assign_slots(class_& cls); @@ -1346,6 +1359,7 @@ void registry::compiler::augment_methods() { for (auto& cls : param->cone) { auto& proxy = method.foreign_classes.emplace_back(); + proxy.foreign_position = &cls - param->cone.data(); proxy.foreign_entry = cls.entry; proxy.foreign_abstract = cls.is_abstract; } @@ -1392,6 +1406,10 @@ void registry::compiler::augment_methods() { << "\n"; indent _(tr); + if constexpr (has_trace) { + trace_foreign_parameters(*first_info); + } + if (rtti::type_index(first_info->return_type_id) != rtti::type_index(rtti::template static_type())) { auto covariant_return_iter = @@ -1617,19 +1635,43 @@ void registry::compiler::augment_foreign_parameters() { }; for (auto& info : registry::static_::st.foreign_parameters) { - // Both registries defer, or neither (see `method`). + // The type ids of the parameter are this registry's to resolve: the + // method, in another registry, resolved them at construction unless + // this one defers them. Only this position - the others follow the + // deferral of their own registries. if constexpr (has_deferred_static_rtti) { - static_cast(*info.method).resolve_type_ids(); + info.resolve_vp(info.method->vp_begin, info.param); for (auto& spec : info.method->overriders) { - static_cast(spec).resolve_type_ids(); + spec.resolve_vp(spec.vp_begin, info.param); } } - auto& param = foreign_parameters.emplace_back(); - param.info = &info; - param.vp = find_class(info.method->vp_begin[info.param]); - ++tr << "parameter " << info.param << ": " << *param.vp << "\n"; + // The same parameter of another copy of the same method. + auto same = std::find_if( + foreign_parameters.begin(), foreign_parameters.end(), + [&info](const foreign_parameter& param) { + auto other = param.copies.front().info; + + return other->method_state == info.method_state && + other->param == info.param && + info.same_method(other->method_type(), info.method_type()); + }); + + auto& param = same != foreign_parameters.end() + ? *same + : foreign_parameters.emplace_back(); + auto is_new = param.copies.empty(); + auto& copy = param.copies.emplace_back(); + copy.info = &info; + + if (is_new) { + param.vp = find_class(info.method->vp_begin[info.param]); + param.vp->used_by_vp.push_back({¶m.slot, nullptr, info.param}); + } + + ++tr << "parameter " << info.param << ": " << *param.vp + << (is_new ? "\n" : " (another copy)\n"); for (auto& spec : info.method->overriders) { auto cls = find_class(spec.vp_begin[info.param]); @@ -1646,13 +1688,69 @@ void registry::compiler::augment_foreign_parameters() { abort(); } - param.overriders.push_back(cls); + copy.overriders.push_back(cls); + } + + { + indent _(tr); + ++tr << "overriders: " << copy.overriders << "\n"; } + } +} + +// What the registries of a method's foreign parameters published for them. +// Their classes are known here only by position in the cone: `foreign#0` is +// the parameter's class. The registry that published them names them in its +// own trace. +template +template +void registry::compiler::trace_foreign_parameters( + const detail::method_info& info) { + using namespace detail; + + for (auto& param : range{info.foreign_begin, info.foreign_end}) { + ++tr << "parameter " << param.param + << " dispatches in another registry, published at generation " + << param.generation << ": slot " << param.slot << "\n"; + indent _(tr); + + for (auto& cls : param.cone) { + ++tr << "foreign#" << (&cls - param.cone.data()) << ": entry at " + << cls.entry << (cls.is_abstract ? ", abstract" : "") + << ", derived:"; - param.vp->used_by_vp.push_back({¶m.slot, nullptr, info.param}); + auto derived = cls.transitive_derived; + std::sort(derived.begin(), derived.end()); + + for (auto position : derived) { + tr << " foreign#" << position; + } + + tr << "\n"; + } } } +// An entry of this registry's v-tables that it leaves empty: a hole, or the +// slot of a parameter of another registry's method, which that registry fills. +template +template +void registry::compiler::trace_unfilled_entry( + class_& cls, std::size_t slot, const detail::word* address) { + for (auto& param : foreign_parameters) { + if (param.slot == slot && + (param.vp == &cls || param.vp->is_base_of(&cls))) { + ++tr << address << " for (method of another registry) parameter " + << param.copies.front().info->param + << ", filled by its registry\n"; + + return; + } + } + + ++tr << "empty\n"; +} + // Slot allocation. The scheme follows the remarks Steven Watanabe made on the // previous allocator, recorded in issue #19: pull the slots a class may not // take from the classes deriving from it, instead of pushing reservations up @@ -1891,7 +1989,7 @@ void registry::compiler::build_dispatch_tables() { if (cls->is_foreign()) { // Written at commit, see write_global_data(). foreign_writes.push_back( - {cls->foreign_entry, &m, dim, group_num, {}}); + {cls->foreign_entry, cls, &m, dim, group_num, {}}); continue; } @@ -2194,7 +2292,12 @@ void registry::compiler::write_global_data() { for (auto& entry : cls.vtbl) { if (entry.method_index == no_method) { - ++tr << "empty\n"; + if constexpr (has_trace) { + trace_unfilled_entry( + cls, cls.first_slot + (&entry - cls.vtbl.data()), + gv_iter); + } + BOOST_ASSERT(gv_iter + 1 <= gv_last); *gv_iter++ = std::size_t(0); @@ -2235,15 +2338,34 @@ void registry::compiler::write_global_data() { // The entries of this registry's methods in other registries' v-tables. // They are written at commit, like everything else. + if (!foreign_writes.empty()) { + ++tr << "Entries in the v-tables of other registries, written at " + "commit:\n"; + } + for (auto& write : foreign_writes) { + indent _(tr); auto& m = *write.method; + ++tr << write.entry << " " << *write.cls << " method #" << m.index + << " "; if (m.arity() == 1) { - write.value = m.dispatch_table[write.group]->pf; - } else if (write.dim == 0) { - write.value = std::uintptr_t(m.gv_dispatch_table + write.group); + auto spec = m.dispatch_table[write.group]; + write.value = spec->pf; + tr << "spec #" << spec->spec_index << "\n"; + indent _2(tr); + ++tr << type_name(m.infos[0]->method_type_id) << "\n"; + ++tr << spec_name(m, spec) << "\n"; } else { - write.value = write.group; + if (write.dim == 0) { + write.value = std::uintptr_t(m.gv_dispatch_table + write.group); + } else { + write.value = write.group; + } + + tr << "vp #" << write.dim << " group #" << write.group << "\n"; + indent _2(tr); + ++tr << type_name(m.infos[0]->method_type_id) << "\n"; } } @@ -2263,10 +2385,11 @@ void registry::compiler::write_global_data() { } } - param.cone.reserve(cone.size()); + auto& first = param.copies.front(); + first.cone.reserve(cone.size()); for (auto cls : cone) { - auto& foreign = param.cone.emplace_back(); + auto& foreign = first.cone.emplace_back(); // The dispatch data is ours; `vptr` is const only for the users of // the v-tables. foreign.entry = const_cast(cls->vptr) + param.slot; @@ -2277,8 +2400,45 @@ void registry::compiler::write_global_data() { } } - for (auto cls : param.overriders) { - param.overrider_positions.push_back(position.at(cls)); + // Each copy gets its own, to swap in at commit, which cannot throw. + for (auto& copy : param.copies) { + if (© != &first) { + copy.cone = first.cone; + } + + for (auto cls : copy.overriders) { + copy.overrider_positions.push_back(position.at(cls)); + } + } + + if constexpr (has_trace) { + if (¶m == &foreign_parameters.front()) { + ++tr << "Publishing to methods of other registries, " + "generation " + << (static_::st.generation + 1) << ":\n"; + } + + indent _(tr); + ++tr << "parameter " << first.info->param << ", slot " << param.slot + << ", " << param.copies.size() + << (param.copies.size() == 1 ? " copy" : " copies") + << " of the method\n"; + indent _2(tr); + + for (auto cls : cone) { + auto& foreign = first.cone[position.at(cls)]; + ++tr << "foreign#" << position.at(cls) << " is " << *cls + << ": entry at " << foreign.entry + << (foreign.is_abstract ? ", abstract" : "") << "\n"; + } + + for (auto& copy : param.copies) { + ++tr << "overriders at positions " + << range{ + copy.overrider_positions.begin(), + copy.overrider_positions.end()} + << "\n"; + } } } @@ -2354,10 +2514,12 @@ void registry::compiler::commit_global_data( auto generation = ++static_::st.generation; for (auto& param : foreign_parameters) { - param.info->slot = param.slot; - param.info->cone.swap(param.cone); - param.info->overriders.swap(param.overrider_positions); - param.info->generation = generation; + for (auto& copy : param.copies) { + copy.info->slot = param.slot; + copy.info->cone.swap(copy.cone); + copy.info->overriders.swap(copy.overrider_positions); + copy.info->generation = generation; + } } for (auto& write : foreign_writes) { diff --git a/include/boost/openmethod/preamble.hpp b/include/boost/openmethod/preamble.hpp index 5d11a741..0daad718 100644 --- a/include/boost/openmethod/preamble.hpp +++ b/include/boost/openmethod/preamble.hpp @@ -441,6 +441,19 @@ struct foreign_parameter_info : std::size_t param; // The initialization count of the parameter's registry. const std::size_t* host_generation; + // Identify the method across modules, each of which may register a copy + // of it: by the state of the method's registry - one symbol per registry - + // and by its type_id, compared by that registry's rtti policy, the only + // one that can. + const void* method_state; + bool (*same_method)(type_id, type_id); + // The method's type_id. A function, because the method's registry may + // defer its type ids, and not have resolved them yet when the parameter's + // registry is initialized - which is after static construction, when any + // rtti policy can answer. + type_id (*method_type)(); + // Like `overrider_info::resolve_vp`, for the method. + void (*resolve_vp)(type_id* ids, std::size_t param); // Published by the parameter's registry. @@ -474,6 +487,11 @@ struct overrider_info : static_list::static_link { type_id type; // of the function, for trace void (**next)(); type_id *vp_begin, *vp_end; + // Set the type_id of the class of a virtual parameter in `vp_begin`, by + // the rtti policy of the registry the parameter dispatches in. Called by + // that registry's initialize() when its type ids are deferred and it is + // not the method's. + void (*resolve_vp)(type_id* ids, std::size_t param); void (*pf)(); // Set by BOOST_OPENMETHOD_INLINE_OVERRIDE (see the Inline template // parameter of override_impl/override_aux and class inline_override in diff --git a/test/compile_fail_mixed_registries_deferred.cpp b/test/compile_fail_mixed_registries_deferred.cpp deleted file mode 100644 index abaae137..00000000 --- a/test/compile_fail_mixed_registries_deferred.cpp +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) 2017-2026 Jean-Louis Leroy -// Distributed under the Boost Software License, Version 1.0. -// See accompanying file LICENSE_1_0.txt -// or copy at http://www.boost.org/LICENSE_1_0.txt) - -// Expected diagnostic, as a CMake regex (see CMakeLists.txt). -// expected-error: deferred static rtti - -#include - -using namespace boost::openmethod; - -// Standard RTTI, with the type ids resolved at initialize() time. -struct deferred_std_rtti : policies::deferred_static_rtti { - template - struct fn : policies::std_rtti::fn {}; -}; - -struct deferred_registry : default_registry::with {}; - -struct Animal { - virtual ~Animal() = default; -}; - -// The parameter's type ids are resolved by its registry, which defers, while -// the method's registry does not: the ids would be read before they exist. -BOOST_OPENMETHOD( - poke, (virtual_), void, default_registry); - -int main() { - // The check is in `method`'s class body. gcc and clang instantiate it - // through the registrar, but MSVC does not: calling the method forces the - // point on every compiler. - Animal animal; - poke(animal); - - return 0; -} diff --git a/test/test_core.cpp b/test/test_core.cpp index c8d7bb70..80af1b90 100644 --- a/test/test_core.cpp +++ b/test/test_core.cpp @@ -135,10 +135,10 @@ static_assert(!detail::is_registry); BOOST_AUTO_TEST_CASE(test_init_type_ids) { type_id ids[2]; - auto last = init_type_ids< + init_type_ids< mp11::mp_list, - mp11::mp_list>::fn(ids); - BOOST_TEST_REQUIRE(last - ids == 2); + mp11::mp_list, + mp11::mp_iota_c<2>>::fn(ids); BOOST_TEST_REQUIRE(ids[0] == type_id(&typeid(a))); BOOST_TEST_REQUIRE(ids[1] == type_id(&typeid(b))); } diff --git a/test/test_mixed_registries.cpp b/test/test_mixed_registries.cpp index 4154178a..9ab1a2df 100644 --- a/test/test_mixed_registries.cpp +++ b/test/test_mixed_registries.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -369,3 +370,57 @@ BOOST_AUTO_TEST_CASE(parameter_registry_initialized_again) { BOOST_TEST(run_over(dog, car) == "dog, car"); BOOST_TEST(carry(car, dog) == "car, dog"); } + +// A program whose modules share a registry may hold several copies of a +// method, one per module, each registering a record of its foreign parameters. +// Fake a second copy of `carry`: its parameter must share the slot of the +// first, rather than take one of its own. +BOOST_AUTO_TEST_CASE(copies_share_the_slot) { + using carry_method = BOOST_OPENMETHOD_TYPE( + carry, + (virtual_, virtual_), + std::string, garage_registry); + auto& real = carry_method::fn; + BOOST_TEST_REQUIRE(real.foreign_end - real.foreign_begin == 1); + auto& real_param = *real.foreign_begin; + + std::size_t slots_strides[3] = {}; + detail::method_info copy{}; + copy.vp_begin = real.vp_begin; + copy.vp_end = real.vp_end; + copy.not_implemented = real.not_implemented; + copy.ambiguous = real.ambiguous; + copy.method_type_id = real.method_type_id; + copy.return_type_id = real.return_type_id; + copy.slots_strides_ptr = slots_strides; + + detail::foreign_parameter_info param{}; + param.method = © + param.param = real_param.param; + param.host_generation = real_param.host_generation; + param.method_state = real_param.method_state; + param.same_method = real_param.same_method; + param.method_type = real_param.method_type; + param.resolve_vp = real_param.resolve_vp; + copy.foreign_begin = ¶m; + copy.foreign_end = ¶m + 1; + + garage_registry::state().methods.push_back(copy); + zoo_registry::state().foreign_parameters.push_back(param); + + initialize_all(); + + BOOST_TEST(param.slot == real_param.slot); + BOOST_TEST_REQUIRE(param.cone.size() == real_param.cone.size()); + BOOST_TEST(param.cone[0].entry == real_param.cone[0].entry); + BOOST_TEST( + std::equal(slots_strides, slots_strides + 3, real.slots_strides_ptr)); + + Car car; + Dog dog; + BOOST_TEST(carry(car, dog) == "car, dog"); + + zoo_registry::state().foreign_parameters.remove(param); + garage_registry::state().methods.remove(copy); + initialize_all(); +} diff --git a/test/test_mixed_registries_deferred.cpp b/test/test_mixed_registries_deferred.cpp new file mode 100644 index 00000000..c5100ef4 --- /dev/null +++ b/test/test_mixed_registries_deferred.cpp @@ -0,0 +1,211 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +// Methods whose virtual parameters mix registries that defer their type ids +// with registries that do not. The animals get their type numbers at run time, +// at the start of the test: a type id read during static construction is zero, +// and dispatch goes wrong. + +#include +#include +#include + +#include +#include +#include + +#define BOOST_TEST_MODULE mixed_registries_deferred +#include + +using namespace boost::openmethod; + +namespace { + +auto next_non_polymorphic_id() -> std::size_t { + static std::size_t counter = 0; + + return (std::size_t(1) << (sizeof(std::size_t) * 8 - 1)) | ++counter; +} + +template +auto non_polymorphic_static_type() -> std::size_t { + static std::size_t value = next_non_polymorphic_id(); + + return value; +} + +struct Animal { + static std::size_t static_type; + std::size_t type; + + Animal() : type(static_type) { + } + + protected: + explicit Animal(std::size_t type) : type(type) { + } +}; + +struct Dog : Animal { + static std::size_t static_type; + + Dog() : Animal(static_type) { + } +}; + +struct Cat : Animal { + static std::size_t static_type; + + Cat() : Animal(static_type) { + } +}; + +std::size_t Animal::static_type; +std::size_t Dog::static_type; +std::size_t Cat::static_type; + +void assign_type_ids() { + Animal::static_type = 1; + Dog::static_type = 2; + Cat::static_type = 3; +} + +struct late_rtti : policies::deferred_static_rtti { + template + struct fn : defaults { + template + static constexpr bool is_polymorphic = std::is_base_of_v; + + template + static auto static_type() -> type_id { + if constexpr (is_polymorphic) { + return type_id(T::static_type); + } else { + return type_id(non_polymorphic_static_type()); + } + } + + template + static auto dynamic_type(const T& obj) -> type_id { + if constexpr (is_polymorphic) { + return type_id(obj.type); + } else { + return type_id(non_polymorphic_static_type()); + } + } + + static auto type_index(type_id type) -> type_id { + return type; + } + }; +}; + +struct Vehicle { + virtual ~Vehicle() = default; +}; + +struct Car : Vehicle {}; +struct Truck : Vehicle {}; + +struct Planet { + virtual ~Planet() = default; +}; + +struct Earth : Planet {}; +struct Mars : Planet {}; + +struct late_registry : + default_registry::with::without::with< + policies::throw_error_handler> {}; + +struct garage_registry : + default_registry::with< + policies::runtime_checks, policies::throw_error_handler> {}; + +struct space_registry : + default_registry::with {}; + +BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat, late_registry); +BOOST_OPENMETHOD_CLASSES(Vehicle, Car, Truck, garage_registry); +BOOST_OPENMETHOD_CLASSES(Planet, Earth, Mars, space_registry); + +// The method's registry defers; so does its only parameter's. +BOOST_OPENMETHOD(sound, (virtual_), std::string, late_registry); + +BOOST_OPENMETHOD_OVERRIDE(sound, (const Dog&), std::string) { + return "bark"; +} + +BOOST_OPENMETHOD_OVERRIDE(sound, (const Cat&), std::string) { + return "meow"; +} + +// The method's registry does not defer; one parameter's does, the first or the +// second. +BOOST_OPENMETHOD( + ride, (virtual_, virtual_), + std::string, garage_registry); + +BOOST_OPENMETHOD_OVERRIDE(ride, (const Dog&, const Car&), std::string) { + return "dog, car"; +} + +BOOST_OPENMETHOD_OVERRIDE(ride, (const Cat&, const Truck&), std::string) { + return "cat, truck"; +} + +BOOST_OPENMETHOD( + drive, (virtual_, virtual_), + std::string, garage_registry); + +BOOST_OPENMETHOD_OVERRIDE(drive, (const Car&, const Cat&), std::string) { + return "car, cat"; +} + +BOOST_OPENMETHOD_OVERRIDE(drive, (const Truck&, const Dog&), std::string) { + return "truck, dog"; +} + +// The method's registry defers; its foreign parameter's does not. +BOOST_OPENMETHOD( + visit, (virtual_, virtual_), + std::string, late_registry); + +BOOST_OPENMETHOD_OVERRIDE(visit, (const Dog&, const Earth&), std::string) { + return "dog, earth"; +} + +BOOST_OPENMETHOD_OVERRIDE(visit, (const Cat&, const Mars&), std::string) { + return "cat, mars"; +} + +} // namespace + +BOOST_AUTO_TEST_CASE(mixed_deferral) { + assign_type_ids(); + + initialize(); + initialize(); + initialize(); + + Dog dog; + Cat cat; + Car car; + Truck truck; + Earth earth; + Mars mars; + + BOOST_TEST(sound(dog) == "bark"); + BOOST_TEST(sound(cat) == "meow"); + + BOOST_TEST(ride(dog, car) == "dog, car"); + BOOST_TEST(ride(cat, truck) == "cat, truck"); + + BOOST_TEST(drive(car, cat) == "car, cat"); + BOOST_TEST(drive(truck, dog) == "truck, dog"); + + BOOST_TEST(visit(dog, earth) == "dog, earth"); + BOOST_TEST(visit(cat, mars) == "cat, mars"); +} From a84fc9f352c4d0b0b04e24c16f3e450da81b245e Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Fri, 25 Sep 2026 08:20:19 -0400 Subject: [PATCH 3/5] doc: the mixed registries example evaluates the expression `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 Claude-Session: https://claude.ai/code/session_011Eo4q4cYTx457W7oaunqsQ --- CLAUDE.md | 47 ++++++++++++---- .../ROOT/examples/mixed_registries.cpp | 54 +++++++++++++++++-- .../ROOT/pages/registries_and_policies.adoc | 23 ++++++-- 3 files changed, 106 insertions(+), 18 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 615ab4be..b1abf55d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -661,8 +661,8 @@ one and the file still compiles, binds to `default_registry`, and fails at run t A virtual parameter dispatches in `detail::dispatch_registry` - what it carries, or the method's registry `R` when it carries nothing - and everything per-parameter goes through that registry: `parameter_traits`, `method::vptr` (the declared parameter is the template argument, not -`remove_virtual_<>` of it), the type ids (`init_type_ids`, one rtti per -position), `init_bad_call` (one rtti per argument). "Another registry" means another *state*: +`remove_virtual_<>` of it), the type ids (`init_type_ids`, one rtti +per position), `init_bad_call` (one rtti per argument). "Another registry" means another *state*: `detail::same_registry` compares `registry_type`, so `struct zoo : default_registry {}` is the default registry under a second name, and a parameter carrying it is native. @@ -695,21 +695,48 @@ id is UB). So a foreign parameter is described to the method's registry by *posi `method::check_foreign_parameters` refuses a call once S has been initialized again. Both raise `parameter_registry_not_initialized`. Consequence: S before R, R again after S, and a cycle (an R method with an S parameter, an S method with an R parameter) cannot be initialized. -- Each module's copy of a method registers its own records, so S allocates one slot per copy; - R uses the first copy's and propagates `slots_strides` as before. The other slots are wasted, - not wrong. -- `has_deferred_static_rtti` must agree between R and every S (static_assert in `method`, test - `compile_fail_mixed_registries_deferred.cpp`): S resolves the method's deferred ids itself. +- Each module's copy of a method registers its own record. S groups them into one + `foreign_parameter` with several `copies`, sharing a slot, by the key (`method_state`, method + type id, `param`). The method's state address is one symbol per registry; the type ids are + compared by `same_method`, which the method template builds from *its* registry's + `rtti::type_index` - S cannot compare them itself, and a raw comparison would fail in exactly + the multi-module case (one `type_info` per module). Each copy still gets its own overrider + positions, since its overrider list is its own. `copies_share_the_slot` in + `test_mixed_registries.cpp` fakes a second copy. +- **Each parameter's type ids follow the deferral of the registry it dispatches in**, never the + method's. `method::resolve_type_ids()` (R's, at construction or in R's `initialize()`) sets the + method's own ids and the *native* positions only (`NativeParameters`). A foreign position whose + registry does not defer is set at construction (`EagerForeignParameters`); one whose registry + defers is set by S's `initialize()`, through `foreign_parameter_info::resolve_vp` for the method + and `overrider_info::resolve_vp` for each overrider - function pointers built by the templates, + since S knows neither type. Neither registry calls the other's rtti during static construction. + The slot-sharing key calls `method_type()` rather than reading `method_type_id`, which a + deferring R has not resolved yet when S initializes; the `deferred_static_rtti` contract only + promises ids from the first `initialize()` on, which S's is. `test_mixed_registries_deferred.cpp` + assigns its ids at the start of the test, so an early read yields 0 and fails. - The C++26 scan (`method_traits_aux`) registers only the classes of the parameters that dispatch in the method's registry (`detail::dispatches_in`). - `rebind_parameter_registry` still rebinds every `virtual_ptr` to the method's registry, so an overrider that spells a third registry on a foreign parameter gets "cannot find method" rather than "registry mismatch". +**The trace follows the handshake across both registries** - `trace()` on each `initialize()` +shows it end to end, and the entry addresses match between the two halves. S prints the overrider +classes of each foreign parameter, says which parameter an entry it leaves empty belongs to rather +than `empty`, and ends with `Publishing to methods of other registries` (generation, slot, number +of module copies, the cone with each class and entry address, the overrider positions). R prints, +under the method, what it received per foreign parameter, names the cone classes `foreign#k` +(`foreign#0` is the parameter's class) wherever a `class_` is printed, and ends with +`Entries in the v-tables of other registries, written at commit`. **A registry with no `output` +policy cannot be traced at all** (`Registry::output::stream()` is `void`) - that is existing +behaviour, but it bites here, because a custom-rtti registry written for an example often has no +`output`. + Tests: `test_mixed_registries.cpp` (two number-based rtti policies that collide on purpose, plus -std_rtti; foreign first, second and only parameter; `next`; errors; re-initialization) and -`test_mixed_registries_affinity.cpp` (the affinity and spelling shapes that used to be -"registry mismatch" compile-fail tests). +std_rtti; foreign first, second and only parameter; `next`; errors; re-initialization; copies +sharing a slot), `test_mixed_registries_affinity.cpp` (the affinity and spelling shapes that used +to be "registry mismatch" compile-fail tests) and `test_mixed_registries_deferred.cpp` (deferred +and eager registries, both ways round). ### Flattened headers for Compiler Explorer diff --git a/doc/modules/ROOT/examples/mixed_registries.cpp b/doc/modules/ROOT/examples/mixed_registries.cpp index c52462ea..8b6db982 100644 --- a/doc/modules/ROOT/examples/mixed_registries.cpp +++ b/doc/modules/ROOT/examples/mixed_registries.cpp @@ -3,6 +3,10 @@ // See accompanying file LICENSE_1_0.txt // or copy at http://www.boost.org/LICENSE_1_0.txt) +#include +#include +#include +#include #include #include @@ -43,6 +47,17 @@ struct Plus : Node { const Node& right; }; +auto next_non_node_type_id() -> type_id { + static auto next = std::numeric_limits::max(); + + return reinterpret_cast(next--); +} + +auto non_node_type_index(type_id type) -> std::size_t { + return std::numeric_limits::max() - + reinterpret_cast(type) + 1; +} + struct node_rtti : policies::rtti { template struct fn : defaults { @@ -54,7 +69,9 @@ struct node_rtti : policies::rtti { if constexpr (is_polymorphic) { return reinterpret_cast(T::static_type); } else { - return nullptr; + static const auto id = next_non_node_type_id(); + + return id; } } @@ -66,6 +83,18 @@ struct node_rtti : policies::rtti { return nullptr; } } + + template + static void type_name(type_id type, Stream& stream) { + static const char* const names[] = {"Node", "Number", "Plus"}; + auto id = reinterpret_cast(type); + + if (id >= 1 && id <= 3) { + stream << names[id - 1]; + } else { + stream << "<" << non_node_type_index(type) << ">"; + } + } }; }; @@ -74,6 +103,18 @@ struct node_registry : registry {}; BOOST_OPENMETHOD_CLASSES(Node, Number, Plus, node_registry); // end::nodes[] +// tag::value[] +BOOST_OPENMETHOD(value, (virtual_), int, node_registry); + +BOOST_OPENMETHOD_OVERRIDE(value, (const Number& number), int) { + return number.value; +} + +BOOST_OPENMETHOD_OVERRIDE(value, (const Plus& plus), int) { + return value(plus.left) + value(plus.right); +} +// end::value[] + // tag::formats[] struct Format { virtual ~Format() = default; @@ -113,11 +154,18 @@ BOOST_AUTO_TEST_CASE(mixed_registries) { initialize(); // end::initialize[] + std::ostringstream captured; + auto* cout_buf = std::cout.rdbuf(captured.rdbuf()); + // tag::call[] Number one(1), two(2), three(3); Plus sum(one, two), total(sum, three); - BOOST_TEST(render(total, Postfix()) == "1 2 + 3 +"); - BOOST_TEST(render(total, Infix()) == "((1 + 2) + 3)"); + std::cout << render(total, Postfix()) << " = " << value(total) << "\n"; + std::cout << render(total, Infix()) << " = " << value(total) << "\n"; // end::call[] + + std::cout.rdbuf(cout_buf); + + BOOST_TEST(captured.str() == "1 2 + 3 + = 6\n((1 + 2) + 3) = 6\n"); } diff --git a/doc/modules/ROOT/pages/registries_and_policies.adoc b/doc/modules/ROOT/pages/registries_and_policies.adoc index 555858b1..7183ab90 100644 --- a/doc/modules/ROOT/pages/registries_and_policies.adoc +++ b/doc/modules/ROOT/pages/registries_and_policies.adoc @@ -176,6 +176,14 @@ own: include::example$mixed_registries.cpp[tag=nodes] ---- +That registry has methods of its own. `value` evaluates a node, and takes +nothing but nodes, so it is an ordinary single-registry method: + +[source,c++] +---- +include::example$mixed_registries.cpp[tag=value] +---- + The output formats use the standard RTTI, and are registered in the default registry: @@ -199,8 +207,9 @@ registry may be any registry - it holds the method and its dispatch table, and need not be one that its parameters carry. A parameter that dispatches in another registry takes a slot in the v-tables of -that registry, which the method's registry fills. The parameter's registry must -therefore be initialized first: +that registry, beside the slots of the methods that registry holds itself, and +the method's registry fills it. The parameter's registry must therefore be +initialized first: [source,c++] ---- @@ -215,21 +224,25 @@ v-tables, and the method's registry has to be initialized again as well. Consequently, two registries cannot each have a method with a parameter that dispatches in the other: neither can be initialized first. -Calls work as usual: +Calls work as usual - `render` dispatching in both registries at once, `value` +in `node_registry` alone: [source,c++] ---- include::example$mixed_registries.cpp[tag=call] ---- +This prints `1 2 + 3 + = 6`, then `((1 + 2) + 3) = 6`. + The type ids of different registries cannot be compared. Two registries may give the same id to different classes, or, with different `rtti` policies, ids of different kinds. The type ids in a cpp:bad_call[] error come each from the registry of its parameter, so an error handler that prints them must use that registry's `rtti` policy for each of them. -A method and the registries that its parameters dispatch in must all use -`deferred_static_rtti`, or none of them. +Registries whose `rtti` policy derives from `deferred_static_rtti` mix with +those whose policy does not: the type ids of a parameter are resolved when its +own registry resolves them. A registry has a collection of _policies_. Each policy belongs to a policy category. A registry may contain at most one policy of each category. Policies From 06aa4f1bbdc7a38e48017ac641079d89329d9683 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Fri, 25 Sep 2026 18:10:11 -0400 Subject: [PATCH 4/5] fix: the affinity test declared a hidden friend with internal linkage `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 Claude-Session: https://claude.ai/code/session_011Eo4q4cYTx457W7oaunqsQ --- test/test_mixed_registries_affinity.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/test_mixed_registries_affinity.cpp b/test/test_mixed_registries_affinity.cpp index ddc3b3bb..11300fe7 100644 --- a/test/test_mixed_registries_affinity.cpp +++ b/test/test_mixed_registries_affinity.cpp @@ -20,8 +20,6 @@ using namespace boost::openmethod; -namespace { - struct zoo_registry : default_registry::with> {}; // Holds the methods. @@ -88,8 +86,6 @@ BOOST_OPENMETHOD_OVERRIDE(adopted, (const Dog&, const Car&), std::string) { return "dog, car"; } -} // namespace - BOOST_AUTO_TEST_CASE(any_method_registry) { initialize(); initialize(); From 45c404b89bbe80b56933be04b830632a13527877 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 26 Sep 2026 15:14:16 -0400 Subject: [PATCH 5/5] fix: unreferenced trace parameter under MSVC 14.2 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 Claude-Session: https://claude.ai/code/session_011Eo4q4cYTx457W7oaunqsQ --- include/boost/openmethod/initialize.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/boost/openmethod/initialize.hpp b/include/boost/openmethod/initialize.hpp index 895b2ab4..9d4a5ac6 100644 --- a/include/boost/openmethod/initialize.hpp +++ b/include/boost/openmethod/initialize.hpp @@ -718,7 +718,7 @@ auto operator<<(trace_stream& tr, const generic_compiler::class_& cls) template class Container, typename... T> auto operator<<( trace_stream& tr, - Container& classes) + [[maybe_unused]] Container& classes) -> trace_stream& { if constexpr (Compiler::has_trace) { tr << "(";