From 89d2338332cab378d662301ea4fd1dda75c08197 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Tue, 1 Sep 2026 15:59:01 -0400 Subject: [PATCH 1/5] virtual parameters obtain their registry by ADL A class can now name the registry it belongs to, once, next to itself: class Animal { friend auto boost_openmethod_registry(Animal*) -> zoo_registry; }; The class then has an *affinity* for that registry, inherited by its derived classes, and everything that mentions it finds it: `virtual_ptr`, its deduction guides, `final_virtual_ptr`, the smart pointer aliases and factories, and any method that takes the class as a virtual parameter. A method declared without a registry argument takes the affinity its virtual parameters agree on. `inplace_vptr.hpp` already had this hook, privately, returning `void` to mean "no customization". Making the catch-all return BOOST_OPENMETHOD_DEFAULT_REGISTRY instead lets it serve as a default template argument directly, and makes backward compatibility structural: with no overload anywhere, every construct resolves to what it resolved to before. `.text` for test_virtual_ptr_dispatch.cpp is byte-identical, and the 530 defined symbols are unchanged. Having *no* affinity is not the same as an affinity for the default registry - only the former yields. That is what lets a method mix a class that has one with a class that has none, so a first affinity does not cascade errors through a codebase. Two conflicting affinities are diagnosed, as is a registry named on a method that contradicts one of its parameters. Deliberately out of scope, and documented as such: `virtual_` keeps its single template parameter; `use_classes` still registers into the macro default unless a registry is listed last; the `any` and `type_erasure` interop headers are untouched. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QoMTqq3duJAptRAbCXgNh1 --- CLAUDE.md | 26 +++ doc/modules/ROOT/examples/adl_registry.cpp | 71 ++++++ .../ROOT/pages/registries_and_policies.adoc | 55 +++++ doc/modules/ROOT/pages/virtual_ptr_alt.adoc | 6 + doc/mrdocs.yml | 1 - include/boost/openmethod/core.hpp | 216 ++++++++++++++++-- include/boost/openmethod/inplace_vptr.hpp | 16 +- .../interop/boost_intrusive_ptr.hpp | 4 +- .../openmethod/interop/std_shared_ptr.hpp | 4 +- .../openmethod/interop/std_unique_ptr.hpp | 4 +- include/boost/openmethod/macros.hpp | 14 +- ...pile_fail_adl_registry_ambiguous_bases.cpp | 32 +++ ...il_adl_registry_conflicting_affinities.cpp | 34 +++ ...le_fail_adl_registry_declared_mismatch.cpp | 29 +++ ...compile_fail_adl_registry_private_base.cpp | 28 +++ test/test_adl_registry.cpp | 90 ++++++++ test/test_adl_registry_hidden_friend.cpp | 57 +++++ test/test_adl_registry_inheritance.cpp | 66 ++++++ test/test_adl_registry_inplace.cpp | 60 +++++ test/test_adl_registry_scan.cpp | 90 ++++++++ test/test_adl_registry_smart_ptr.cpp | 79 +++++++ test/test_adl_registry_static_rtti.cpp | 61 +++++ test/test_adl_registry_two_registries.cpp | 83 +++++++ 23 files changed, 1101 insertions(+), 25 deletions(-) create mode 100644 doc/modules/ROOT/examples/adl_registry.cpp create mode 100644 test/compile_fail_adl_registry_ambiguous_bases.cpp create mode 100644 test/compile_fail_adl_registry_conflicting_affinities.cpp create mode 100644 test/compile_fail_adl_registry_declared_mismatch.cpp create mode 100644 test/compile_fail_adl_registry_private_base.cpp create mode 100644 test/test_adl_registry.cpp create mode 100644 test/test_adl_registry_hidden_friend.cpp create mode 100644 test/test_adl_registry_inheritance.cpp create mode 100644 test/test_adl_registry_inplace.cpp create mode 100644 test/test_adl_registry_scan.cpp create mode 100644 test/test_adl_registry_smart_ptr.cpp create mode 100644 test/test_adl_registry_static_rtti.cpp create mode 100644 test/test_adl_registry_two_registries.cpp diff --git a/CLAUDE.md b/CLAUDE.md index 7fd982b2..29ef7ccf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -488,6 +488,32 @@ rules: `missing template arguments` - but a name that *does* resolve would bind to the wrong type silently. `::registry` works. +### Registry affinity + +A class can name its registry instead of the program naming one for every class: declare +`auto boost_openmethod_registry(Class*) -> Registry;`, preferably as a hidden friend. The class +then has an *affinity* for that registry, inherited by its derived classes, and +`default_registry_of` reads it back. `virtual_ptr` and the smart pointer aliases default to it, +and a method declared without a registry argument takes the affinity its virtual parameters agree +on (`detail::method_registry`, driven by `detail::param_affinity` / `agreed_affinity`). + +Having *no* affinity is not the same as an affinity for the default registry: only the former +yields, which is what lets a method mix a class that has one with a class that has none. +`detail::affinity_of` is where that mapping happens; nothing public returns an affinity. + +Two things deliberately do **not** participate, and both are documented as such: + +- `use_classes` / `BOOST_OPENMETHOD_CLASSES` still register into + `BOOST_OPENMETHOD_DEFAULT_REGISTRY` unless a registry is listed last. Registering a class that + has an affinity without naming its registry is a run-time `missing_class`, not a compile error. +- The `any` and `type_erasure` interop headers are untouched. `virtual_any&` contributes no + affinity, so a method over one behaves exactly as before. + +**A test that selects a registry through an affinity needs no PCH marker.** The scan below exists +because `BOOST_OPENMETHOD_DEFAULT_REGISTRY` must be defined before `core.hpp` is parsed, and a +force-included PCH parses it first. An affinity has no such ordering relation to the library +headers, so those tests can share the PCH - do not add a fourth marker for them. + `test/CMakeLists.txt` withholds the shared PCH from any `test_*.cpp` that overrides the registry - a force-included PCH would still precede the `#define`. It detects them by scanning for the token `BOOST_OPENMETHOD_DEFAULT_REGISTRY` **or** for an include of a header that diff --git a/doc/modules/ROOT/examples/adl_registry.cpp b/doc/modules/ROOT/examples/adl_registry.cpp new file mode 100644 index 00000000..03d6bcc1 --- /dev/null +++ b/doc/modules/ROOT/examples/adl_registry.cpp @@ -0,0 +1,71 @@ +// 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 + +#define BOOST_TEST_MODULE adl_registry +#include + +// tag::registry[] +struct zoo_registry + : boost::openmethod::default_registry::with< + boost::openmethod::policies::runtime_checks> {}; +// end::registry[] + +using namespace boost::openmethod; + +// tag::affinity[] +namespace zoo { + +class Animal { + public: + virtual ~Animal() = default; + + private: + // Animal - and every class derived from it - belongs to zoo_registry + friend auto boost_openmethod_registry(Animal*) -> zoo_registry; +}; + +class Dog : public Animal {}; +class Cat : public Animal {}; + +} // namespace zoo +// end::affinity[] + +// tag::methods[] +BOOST_OPENMETHOD_CLASSES(zoo::Animal, zoo::Dog, zoo::Cat, zoo_registry); + +// no registry argument: speak follows Animal +BOOST_OPENMETHOD(speak, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(speak, (const zoo::Dog&), std::string) { + return "bark"; +} + +BOOST_OPENMETHOD_OVERRIDE(speak, (const zoo::Cat&), std::string) { + return "meow"; +} +// end::methods[] + +// tag::virtual_ptr[] +// ...and so does virtual_ptr +static_assert( + std::is_same_v, virtual_ptr>); +// end::virtual_ptr[] + +BOOST_AUTO_TEST_CASE(adl_registry) { + // tag::call[] + initialize(); + + zoo::Dog spot; + zoo::Cat felix; + // end::call[] + + BOOST_TEST(speak(spot) == "bark"); + BOOST_TEST(speak(felix) == "meow"); +} diff --git a/doc/modules/ROOT/pages/registries_and_policies.adoc b/doc/modules/ROOT/pages/registries_and_policies.adoc index d3fa90a6..3f43481b 100644 --- a/doc/modules/ROOT/pages/registries_and_policies.adoc +++ b/doc/modules/ROOT/pages/registries_and_policies.adoc @@ -50,6 +50,61 @@ declaration must use the same class-key as the definition. Qualify the name if it could also be found in namespace `boost::openmethod` - `registry` in particular. +### Registry affinity + +`BOOST_OPENMETHOD_DEFAULT_REGISTRY` is a whole-program answer: one registry, for +every class. A class can instead name its own, by declaring a +cpp:boost_openmethod_registry[] function that takes a pointer to it and returns +the registry. The function is never called - only its return type is used - so +it needs no definition: + +[source,c++] +---- +include::example$adl_registry.cpp[tag=affinity] +---- + +The class then has an _affinity_ for that registry, and everything that mentions +the class finds it. A method declared without a registry argument takes the +affinity of its virtual parameters: + +[source,c++] +---- +include::example$adl_registry.cpp[tag=methods] +---- + +...and so does cpp:virtual_ptr[], along with the smart pointer aliases and the +`make_*_virtual` factories: + +[source,c++] +---- +include::example$adl_registry.cpp[tag=virtual_ptr] +---- + +An affinity is inherited: declaring it for the root of a hierarchy covers every +class derived from it, because the derived-to-base pointer conversion makes the +root's declaration viable. A declaration for a derived class is a better match, +and wins. + +A class that declares nothing has *no* affinity, which is not the same as an +affinity for the default registry. Only the first yields: a method may mix a +class that has an affinity with one that has none, and lands in the former's +registry. Two virtual parameters with *different* affinities are an error, as is +a registry named on the method that contradicts one of its parameters. + +A hidden friend, as above, is the spelling to prefer. The declaration must +precede every use of the class in a method, a `virtual_ptr` or a class +registration - declaring it later makes the program ill-formed with no +diagnostic required, and compilers disagree silently about which registry the +earlier use got. Being part of the class, a hidden friend cannot be late. + +WARNING: An affinity does not reach +xref:reference:BOOST_OPENMETHOD_CLASSES.adoc[BOOST_OPENMETHOD_CLASSES], which +still registers into `BOOST_OPENMETHOD_DEFAULT_REGISTRY` unless a registry is +listed last. Registering a class with an affinity, without naming its registry, +puts the class in one registry and its methods in another - and that shows up as +a `missing_class` error at run time, not as a compile error. List the registry: +`BOOST_OPENMETHOD_CLASSES(Animal, Dog, zoo_registry)`. + 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/doc/modules/ROOT/pages/virtual_ptr_alt.adoc b/doc/modules/ROOT/pages/virtual_ptr_alt.adoc index 29b45cdb..5798b7f3 100644 --- a/doc/modules/ROOT/pages/virtual_ptr_alt.adoc +++ b/doc/modules/ROOT/pages/virtual_ptr_alt.adoc @@ -108,6 +108,12 @@ v-table for the bases, just like what C++ does for its native vptrs. `inplace_vptr_base` and `inplace_vptr_derived` are aliased in `namespace boost::openmethod::aliases`. +`inplace_vptr_base` also declares the class's +xref:ROOT:registries_and_policies.adoc#registries_and_policies[registry affinity], +as a hidden friend - it is told the registry, and every other construct can then +find it. A method over such a hierarchy needs no registry argument, and the +program needs no `BOOST_OPENMETHOD_DEFAULT_REGISTRY` override. + An object that embeds its v-table pointer does not need to be wrapped in a `virtual_ptr` - the two fill the same goal, fast access to the v-table pointer - and wrapping one is rejected at compile time. diff --git a/doc/mrdocs.yml b/doc/mrdocs.yml index 3e4382f5..4edb9450 100644 --- a/doc/mrdocs.yml +++ b/doc/mrdocs.yml @@ -22,7 +22,6 @@ exclude-symbols: - 'boost::openmethod::registry::initialize' - 'boost::openmethod::registry::finalize' - 'boost::openmethod::boost_openmethod_bases' - - 'boost::openmethod::boost_openmethod_registry' - 'boost::openmethod::registry_state::st' # Macros. Only the public macros carry a doc comment, and with diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index fe54174e..f3535079 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -129,11 +129,130 @@ namespace boost::openmethod { #endif namespace detail { + using sfinae = void; -} + +using macro_default_registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY; + +template +constexpr bool false_t = false; // workaround before CWG2518/P2593R1 + +} // namespace detail + +//! Return the registry a class belongs to (ADL customization point). +//! +//! This declaration is a catch-all that matches any argument list and returns +//! @ref BOOST_OPENMETHOD_DEFAULT_REGISTRY, denoting the absence of +//! customization. If an overload beats it for a given class, that class has an +//! *affinity* for the overload's return type, and that registry becomes the +//! default for every construct that mentions the class: @ref virtual_ptr and +//! the smart pointer aliases, and the methods that take it as a virtual +//! parameter. +//! +//! An affinity declared for a class extends to its derived classes, because +//! the derived-to-base pointer conversion makes the base's overload viable. An +//! overload on the derived class itself is a better match, and wins. +//! +//! @par Requirements +//! +//! The library uses argument-dependent lookup to find an overload that +//! satisfies the following requirements: +//! +//! @li The single parameter is a pointer to the class. Its role is to carry the +//! class to the overload. It must not be dereferenced - the function is never +//! called, only its return type is used. +//! +//! @li The return type is a @ref registry. +//! +//! The overload must be declared before the first construct that mentions the +//! class. Declaring it afterwards makes the program ill-formed, no diagnostic +//! required - compilers disagree silently on which registry the earlier +//! construct used. Declaring it as a hidden friend, as below, makes that +//! impossible. +//! +//! @par Example +//! +//! include:../examples/adl_registry.cpp#affinity +//! +//! @see @ref default_registry_of +//! @see [Registries and Policies](xref:ROOT:registries_and_policies.adoc) +auto boost_openmethod_registry(...) -> BOOST_OPENMETHOD_DEFAULT_REGISTRY; + +namespace detail { + +// The catch-all is viable for every class, so the primary template is reached +// only when the call to `boost_openmethod_registry` is ill-formed rather than +// unmatched. Three ways to get there, all of them a base class whose overload +// cannot be used: two base classes with different affinities (ambiguous +// overload), a private base (inaccessible conversion), and a repeated +// non-virtual base (ambiguous conversion). Declaring the overload for the +// class itself resolves all three. +template +struct adl_registry_aux { + static_assert( + false_t, + "cannot tell which registry this class belongs to: " + "boost_openmethod_registry is ambiguous or inaccessible for it - " + "declare one for the class itself"); + using type = macro_default_registry; +}; + +template +struct adl_registry_aux< + Class, + std::void_t()))>> { + using type = decltype(boost_openmethod_registry(std::declval())); +}; + +// The type whose namespace and base classes are consulted. `virtual_traits::virtual_type` would be the exact answer, but it needs the very +// `Registry` being computed. Probing for `element_type` covers `std:: +// shared_ptr`, `std::unique_ptr` and `boost::intrusive_ptr` without depending +// on their headers. +template +struct registry_anchor { + using type = T; +}; + +template +struct registry_anchor> { + using type = typename T::element_type; +}; + +template +using unadorned = std::remove_cv_t< + std::remove_pointer_t>>>; + +template +using adl_registry = + typename adl_registry_aux>::type>>::type; + +} // namespace detail + +//! The registry a class belongs to by default. +//! +//! Evaluates to the return type of the @ref boost_openmethod_registry overload +//! found for `T` by argument-dependent lookup, after removing cv-qualifiers, +//! references and pointers, and unwrapping one level of smart pointer - so +//! `Class`, `const Class&`, `Class*` and `std::shared_ptr` all yield the +//! same registry. +//! +//! If `T`\'s class has no affinity, this is @ref +//! BOOST_OPENMETHOD_DEFAULT_REGISTRY. Note that this is a query, and always +//! answers; having *no* affinity is not the same as having an affinity for the +//! default registry, a distinction that matters when a method draws its +//! registry from its virtual parameters. +//! +//! @tparam T A class, or a reference, pointer or smart pointer to one. +//! +//! @see @ref boost_openmethod_registry +//! @see [Registries and Policies](xref:ROOT:registries_and_policies.adoc) +template +using default_registry_of = detail::adl_registry; template< - class Class, class Registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY, + class Class, class Registry = default_registry_of, typename = detail::sfinae> class virtual_ptr; @@ -142,8 +261,6 @@ class virtual_ptr; namespace detail { -using macro_default_registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY; - template struct extract_registry; @@ -826,7 +943,7 @@ inline auto final_virtual_ptr(Arg&& obj) { // doesn't like it. template inline auto final_virtual_ptr(Arg&& obj) { - return final_virtual_ptr( + return final_virtual_ptr, Arg>( std::forward(obj)); } //! Wide pointer combining pointers to an object and its v-table @@ -1717,7 +1834,7 @@ class virtual_ptr< //! @return A `virtual_ptr`. template virtual_ptr(Class& obj) - -> virtual_ptr; + -> virtual_ptr>; //! Construct a `virtual_ptr` from a xvalue reference. //! @@ -1726,7 +1843,7 @@ virtual_ptr(Class& obj) //! @return A `virtual_ptr`. template virtual_ptr(Class&& obj) - -> virtual_ptr; + -> virtual_ptr>; // Alas this is not allowed: // template @@ -1951,9 +2068,6 @@ template struct parameter_traits&, Registry> : virtual_traits&, Registry> {}; -template -constexpr bool false_t = false; // workaround before CWG2518/P2593R1 - template struct validate_method_parameter : std::true_type {}; @@ -2063,14 +2177,88 @@ struct validate_method_parameter< //! selected is not specified, but it is the same across calls with the //! same arguments types. //! +namespace detail { + +// A class that never declared `boost_openmethod_registry` has no affinity for +// any registry, which is not the same as an affinity for the default one. Only +// the former yields to a parameter that does have one, which is what lets a +// method mix a class that has an affinity with one that has none. +struct no_affinity; + +template +using affinity_of = std::conditional_t< + std::is_same_v, no_affinity, Registry>; + +template +struct param_affinity { + using type = no_affinity; +}; + +template +struct param_affinity> { + using type = affinity_of>; +}; + +template +struct param_affinity> { + using type = affinity_of; +}; + +template +struct param_affinity&> { + using type = affinity_of; +}; + +template +struct param_affinity&> { + using type = affinity_of; +}; + +// The first affinity in the parameter list wins; every other one must agree. +template +struct agreed_affinity { + using type = no_affinity; +}; + +template +struct agreed_affinity { + using rest = typename agreed_affinity::type; + static_assert( + std::is_same_v || + std::is_same_v || + std::is_same_v, + "virtual parameters have conflicting registry affinities"); + using type = std:: + conditional_t, rest, Affinity>; +}; + +// The registry a method takes when its declaration does not name one. +template +struct method_registry_aux { + using type = macro_default_registry; +}; + +template +struct method_registry_aux { + using found = typename agreed_affinity< + typename param_affinity::type...>::type; + using type = std::conditional_t< + std::is_same_v, macro_default_registry, found>; +}; + +template +using method_registry = typename method_registry_aux::type; + +} // namespace detail + //! @tparam Id A type //! @tparam Fn A function type -//! @tparam Registry The registry in which the method is defined +//! @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. //! //! @see [Core API](xref:ROOT:core_api.adoc) -template< - typename Id, typename Fn, - class Registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY> +template> class method; //! Method with a specific id, signature and return type diff --git a/include/boost/openmethod/inplace_vptr.hpp b/include/boost/openmethod/inplace_vptr.hpp index f874c0d5..51b2a75b 100644 --- a/include/boost/openmethod/inplace_vptr.hpp +++ b/include/boost/openmethod/inplace_vptr.hpp @@ -10,12 +10,14 @@ namespace boost::openmethod { namespace detail { -void boost_openmethod_registry(...); void boost_openmethod_bases(...); +// `inplace_vptr_base` declares the affinity that `default_registry_of` reads +// back, so the two are the same question. There is deliberately no catch-all +// here: the one in core.hpp serves both, and a second one returning `void` +// would shadow it for lookups from this namespace. template -using inplace_vptr_registry = - decltype(boost_openmethod_registry(std::declval())); +using inplace_vptr_registry = default_registry_of; template struct update_vptr_bases; @@ -216,6 +218,14 @@ class inplace_vptr_derived { (!detail::is_registry && ...), "registry can be specified only for root classes"); + // Without this, a `Base1` that is not an inplace_vptr root would silently + // acquire the default registry from the catch-all - before the hook was + // unified, `inplace_vptr_registry` was `void` and the mistake was a + // hard error. The single-base specialization asserts the same thing. + static_assert( + std::is_base_of_v, + "class must inherit from inplace_vptr_base"); + friend auto boost_openmethod_registry(Class*) -> detail::inplace_vptr_registry; friend auto boost_openmethod_bases(Class*) diff --git a/include/boost/openmethod/interop/boost_intrusive_ptr.hpp b/include/boost/openmethod/interop/boost_intrusive_ptr.hpp index 1cb36db2..4e7e9c57 100644 --- a/include/boost/openmethod/interop/boost_intrusive_ptr.hpp +++ b/include/boost/openmethod/interop/boost_intrusive_ptr.hpp @@ -129,7 +129,7 @@ struct virtual_traits&, Registry> { //! include:intrusive_ptr.cpp#boost_intrusive_virtual_ptr_alias //! //! @see [Smart Pointers](xref:ROOT:smart_pointers.adoc) -template +template> using boost_intrusive_virtual_ptr = virtual_ptr, Registry>; @@ -153,7 +153,7 @@ using boost_intrusive_virtual_ptr = //! //! @see [Smart Pointers](xref:ROOT:smart_pointers.adoc) template< - class Class, class Registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY, + class Class, class Registry = default_registry_of, typename... T> inline auto make_boost_intrusive_virtual(T&&... args) { return final_virtual_ptr(intrusive_ptr( diff --git a/include/boost/openmethod/interop/std_shared_ptr.hpp b/include/boost/openmethod/interop/std_shared_ptr.hpp index 49f69a84..02b35306 100644 --- a/include/boost/openmethod/interop/std_shared_ptr.hpp +++ b/include/boost/openmethod/interop/std_shared_ptr.hpp @@ -202,7 +202,7 @@ struct virtual_traits&, Registry> { //! include:smart_pointers.cpp#shared_virtual_ptr_alias //! //! @see [Smart Pointers](xref:ROOT:smart_pointers.adoc) -template +template> using shared_virtual_ptr = virtual_ptr, Registry>; //! Create a new object and return a `shared_virtual_ptr` to it. @@ -225,7 +225,7 @@ using shared_virtual_ptr = virtual_ptr, Registry>; //! //! @see [Smart Pointers](xref:ROOT:smart_pointers.adoc) template< - class Class, class Registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY, + class Class, class Registry = default_registry_of, typename... T> inline auto make_shared_virtual(T&&... args) { return final_virtual_ptr( diff --git a/include/boost/openmethod/interop/std_unique_ptr.hpp b/include/boost/openmethod/interop/std_unique_ptr.hpp index a6128723..b99a6e52 100644 --- a/include/boost/openmethod/interop/std_unique_ptr.hpp +++ b/include/boost/openmethod/interop/std_unique_ptr.hpp @@ -72,7 +72,7 @@ struct virtual_traits, Registry> { //! include:smart_pointers.cpp#unique_virtual_ptr_alias //! //! @see [Smart Pointers](xref:ROOT:smart_pointers.adoc) -template +template> using unique_virtual_ptr = virtual_ptr, Registry>; //! Create a new object and return a `unique_virtual_ptr` to it. @@ -95,7 +95,7 @@ using unique_virtual_ptr = virtual_ptr, Registry>; //! //! @see [Smart Pointers](xref:ROOT:smart_pointers.adoc) template< - class Class, class Registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY, + class Class, class Registry = default_registry_of, typename... T> inline auto make_unique_virtual(T&&... args) { return final_virtual_ptr( diff --git a/include/boost/openmethod/macros.hpp b/include/boost/openmethod/macros.hpp index fa26572a..4181550c 100644 --- a/include/boost/openmethod/macros.hpp +++ b/include/boost/openmethod/macros.hpp @@ -25,16 +25,26 @@ struct enable_forwarder< template struct va_args; +// `registry_for` is an alias template, not a typedef, so that the scan for an +// affinity among the virtual parameters does not run for a declaration that +// names a registry. `registry` is retained: it is the registry a declaration +// *names*, which is no longer the same question. template struct va_args { using return_type = ReturnType; using registry = macro_default_registry; + + template + using registry_for = method_registry; }; template struct va_args { using return_type = ReturnType; using registry = Registry; + + template + using registry_for = Registry; }; template @@ -119,7 +129,9 @@ inline constexpr bool method_not_found = false; BOOST_OPENMETHOD_ID(ID), \ ::boost::openmethod::detail::va_args<__VA_ARGS__>::return_type \ PARAMETERS, \ - ::boost::openmethod::detail::va_args<__VA_ARGS__>::registry> + ::boost::openmethod::detail::va_args<__VA_ARGS__>::registry_for< \ + ::boost::openmethod::detail::va_args<__VA_ARGS__>::return_type \ + PARAMETERS>> //! Declare a method. //! diff --git a/test/compile_fail_adl_registry_ambiguous_bases.cpp b/test/compile_fail_adl_registry_ambiguous_bases.cpp new file mode 100644 index 00000000..7222ee4f --- /dev/null +++ b/test/compile_fail_adl_registry_ambiguous_bases.cpp @@ -0,0 +1,32 @@ +// 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: ambiguous or inaccessible + +#include + +using namespace boost::openmethod; + +struct zoo_registry : default_registry {}; +struct kennel_registry : default_registry {}; + +struct Animal { + virtual ~Animal() = default; +}; + +struct Pet { + virtual ~Pet() = default; +}; + +auto boost_openmethod_registry(Animal*) -> zoo_registry; +auto boost_openmethod_registry(Pet*) -> kennel_registry; + +// Inherits two different affinities, and says nothing itself. +struct Dog : Animal, Pet {}; + +int main() { + (void)sizeof(virtual_ptr); +} diff --git a/test/compile_fail_adl_registry_conflicting_affinities.cpp b/test/compile_fail_adl_registry_conflicting_affinities.cpp new file mode 100644 index 00000000..49ccba8c --- /dev/null +++ b/test/compile_fail_adl_registry_conflicting_affinities.cpp @@ -0,0 +1,34 @@ +// 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: conflicting registry affinities + +#include + +#include + +using namespace boost::openmethod; + +struct zoo_registry : default_registry {}; +struct garage_registry : default_registry {}; + +struct Animal { + virtual ~Animal() = default; + friend auto boost_openmethod_registry(Animal*) -> zoo_registry; +}; + +struct Vehicle { + virtual ~Vehicle() = default; + friend auto boost_openmethod_registry(Vehicle*) -> garage_registry; +}; + +// A method cannot span two registries, and its parameters say two different +// things. Naming one on the declaration is the way to settle it. +BOOST_OPENMETHOD( + collide, (virtual_, virtual_), std::string); + +int main() { +} diff --git a/test/compile_fail_adl_registry_declared_mismatch.cpp b/test/compile_fail_adl_registry_declared_mismatch.cpp new file mode 100644 index 00000000..411e33ee --- /dev/null +++ b/test/compile_fail_adl_registry_declared_mismatch.cpp @@ -0,0 +1,29 @@ +// 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() { +} diff --git a/test/compile_fail_adl_registry_private_base.cpp b/test/compile_fail_adl_registry_private_base.cpp new file mode 100644 index 00000000..392f85df --- /dev/null +++ b/test/compile_fail_adl_registry_private_base.cpp @@ -0,0 +1,28 @@ +// 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: ambiguous or inaccessible + +#include + +using namespace boost::openmethod; + +struct zoo_registry : default_registry {}; + +struct Animal { + virtual ~Animal() = default; +}; + +auto boost_openmethod_registry(Animal*) -> zoo_registry; + +// The conversion to `Animal*` is inaccessible, so the base's overload cannot +// answer for `Vault` - and neither can the catch-all, which is a worse match. +// The class has to declare its own. +struct Vault : private Animal {}; + +int main() { + (void)sizeof(virtual_ptr); +} diff --git a/test/test_adl_registry.cpp b/test/test_adl_registry.cpp new file mode 100644 index 00000000..36861404 --- /dev/null +++ b/test/test_adl_registry.cpp @@ -0,0 +1,90 @@ +// 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 class declares an affinity for a registry once, next to itself. Everything +// that mentions the class then finds that registry on its own: `virtual_ptr`, +// and the methods that take the class as a virtual parameter. + +#include + +#include +#include + +#define BOOST_TEST_MODULE adl_registry +#include + +using namespace boost::openmethod; + +struct zoo_registry : default_registry::with {}; + +namespace zoo { + +struct Animal { + virtual ~Animal() = default; +}; + +auto boost_openmethod_registry(Animal*) -> zoo_registry; + +struct Dog : Animal {}; +struct Cat : Animal {}; + +} // namespace zoo + +// A class that never declared one keeps the default registry. +struct Widget { + virtual ~Widget() = default; +}; + +using zoo::Animal, zoo::Dog, zoo::Cat; + +static_assert(std::is_same_v, zoo_registry>); +static_assert(std::is_same_v, zoo_registry>); +static_assert(std::is_same_v, default_registry>); + +// `virtual_ptr` picks it up, so `virtual_ptr` is not a `virtual_ptr` in +// the default registry. +static_assert(std::is_same_v, virtual_ptr>); +static_assert( + std::is_same_v, virtual_ptr>); + +BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat, zoo_registry); + +// Neither declaration names a registry; both land in `zoo_registry`. +BOOST_OPENMETHOD(speak, (virtual_), std::string); +BOOST_OPENMETHOD(poke, (virtual_ptr), std::string); + +static_assert(std::is_same_v< + BOOST_OPENMETHOD_TYPE(speak, (virtual_), std::string), + method< + BOOST_OPENMETHOD_ID(speak), std::string(virtual_), + zoo_registry>>); + +BOOST_OPENMETHOD_OVERRIDE(speak, (const Dog&), std::string) { + return "bark"; +} + +BOOST_OPENMETHOD_OVERRIDE(speak, (const Cat&), std::string) { + return "meow"; +} + +BOOST_OPENMETHOD_OVERRIDE(poke, (virtual_ptr), std::string) { + return "woof"; +} + +BOOST_OPENMETHOD_OVERRIDE(poke, (virtual_ptr), std::string) { + return "hiss"; +} + +BOOST_AUTO_TEST_CASE(dispatch_in_the_registry_the_class_names) { + initialize(); + + Dog spot; + Cat felix; + + BOOST_TEST(speak(spot) == "bark"); + BOOST_TEST(speak(felix) == "meow"); + BOOST_TEST(poke(virtual_ptr(spot)) == "woof"); + BOOST_TEST(poke(virtual_ptr(felix)) == "hiss"); +} diff --git a/test/test_adl_registry_hidden_friend.cpp b/test/test_adl_registry_hidden_friend.cpp new file mode 100644 index 00000000..0efb6858 --- /dev/null +++ b/test/test_adl_registry_hidden_friend.cpp @@ -0,0 +1,57 @@ +// 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) + +// The recommended spelling is a hidden friend: it is part of the class, so no +// use of the class can precede it, and the declaration-order rule cannot be +// broken. Argument-dependent lookup finds it from anywhere. + +#include + +#include +#include + +#define BOOST_TEST_MODULE adl_registry_hidden_friend +#include + +using namespace boost::openmethod; + +struct zoo_registry : default_registry::with {}; + +namespace zoo { + +class Animal { + public: + virtual ~Animal() = default; + + private: + friend auto boost_openmethod_registry(Animal*) -> zoo_registry; +}; + +class Dog : public Animal {}; + +} // namespace zoo + +static_assert(std::is_same_v, zoo_registry>); +static_assert(std::is_same_v, zoo_registry>); + +// The method lives in a third namespace, and still finds the affinity. +namespace vet { + +BOOST_OPENMETHOD_CLASSES(zoo::Animal, zoo::Dog, zoo_registry); + +BOOST_OPENMETHOD(examine, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(examine, (const zoo::Dog&), std::string) { + return "healthy dog"; +} + +} // namespace vet + +BOOST_AUTO_TEST_CASE(a_hidden_friend_cannot_be_declared_late) { + initialize(); + + zoo::Dog rex; + BOOST_TEST(vet::examine(rex) == "healthy dog"); +} diff --git a/test/test_adl_registry_inheritance.cpp b/test/test_adl_registry_inheritance.cpp new file mode 100644 index 00000000..1e57f7f0 --- /dev/null +++ b/test/test_adl_registry_inheritance.cpp @@ -0,0 +1,66 @@ +// 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) + +// An affinity declared for a base class reaches its derived classes, because +// the derived-to-base pointer conversion makes the base's overload viable. A +// class whose base overload cannot be used - inaccessible or ambiguous - is +// diagnosed rather than defaulted: see compile_fail_adl_registry_*.cpp. + +#include + +#define BOOST_TEST_MODULE adl_registry_inheritance +#include + +using namespace boost::openmethod; + +struct zoo_registry : default_registry::with {}; +struct kennel_registry : default_registry::with {}; + +namespace zoo { + +struct Animal { + virtual ~Animal() = default; +}; + +auto boost_openmethod_registry(Animal*) -> zoo_registry; + +struct Cat : Animal {}; +struct Persian : Cat {}; + +// An overload on the class itself is an exact match, and beats the base's. +struct Dog : Animal {}; +auto boost_openmethod_registry(Dog*) -> kennel_registry; +struct Poodle : Dog {}; + +// A virtual base is unambiguous, so it still reaches the affinity. +struct Left : virtual Animal {}; +struct Right : virtual Animal {}; +struct Chimera : Left, Right {}; + +} // namespace zoo + +using namespace zoo; + +// inherited, however deep +static_assert(std::is_same_v, zoo_registry>); +static_assert(std::is_same_v, zoo_registry>); + +// the exact match wins, and is itself inherited +static_assert(std::is_same_v, kennel_registry>); +static_assert(std::is_same_v, kennel_registry>); + +// one Animal, so one affinity +static_assert(std::is_same_v, zoo_registry>); + +// An unrelated class is untouched by any of it. +struct Widget { + virtual ~Widget() = default; +}; + +static_assert(std::is_same_v, default_registry>); + +BOOST_AUTO_TEST_CASE(inheritance_is_compile_time_only) { + BOOST_TEST(true); +} diff --git a/test/test_adl_registry_inplace.cpp b/test/test_adl_registry_inplace.cpp new file mode 100644 index 00000000..98281244 --- /dev/null +++ b/test/test_adl_registry_inplace.cpp @@ -0,0 +1,60 @@ +// 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) + +// `inplace_vptr_base` declares the affinity itself, as a hidden friend. Since +// the hook is now the library's own, a method over such a class needs neither a +// registry argument nor a BOOST_OPENMETHOD_DEFAULT_REGISTRY override. + +#include + +#include +#include +#include + +#define BOOST_TEST_MODULE adl_registry_inplace +#include + +namespace bom = boost::openmethod; + +// An inplace_vptr hierarchy needs neither a vptr policy nor a type hash. +struct zoo_registry + : bom::default_registry::without { +}; + +struct Animal : bom::inplace_vptr_base {}; +struct Dog : Animal, bom::inplace_vptr_derived {}; +struct Cat : Animal, bom::inplace_vptr_derived {}; + +// The mixin's hidden friend is what `default_registry_of` reads back. +static_assert(std::is_same_v, zoo_registry>); +static_assert(std::is_same_v, zoo_registry>); + +// No registry named, and no #define: the method follows the class. +BOOST_OPENMETHOD(speak, (bom::virtual_), std::string); + +static_assert(std::is_same_v< + BOOST_OPENMETHOD_TYPE( + speak, (bom::virtual_), std::string), + bom::method< + BOOST_OPENMETHOD_ID(speak), + std::string(bom::virtual_), zoo_registry>>); + +BOOST_OPENMETHOD_OVERRIDE(speak, (const Dog&), std::string) { + return "bark"; +} + +BOOST_OPENMETHOD_OVERRIDE(speak, (const Cat&), std::string) { + return "meow"; +} + +BOOST_AUTO_TEST_CASE(inplace_vptr_supplies_the_affinity) { + bom::initialize(); + + Dog spot; + Cat felix; + + BOOST_TEST(speak(spot) == "bark"); + BOOST_TEST(speak(felix) == "meow"); +} diff --git a/test/test_adl_registry_scan.cpp b/test/test_adl_registry_scan.cpp new file mode 100644 index 00000000..8e865c37 --- /dev/null +++ b/test/test_adl_registry_scan.cpp @@ -0,0 +1,90 @@ +// 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) + +// How a method picks its registry: it takes the affinity its virtual +// parameters agree on. A class with no affinity contributes nothing, so it +// yields rather than conflicting. A registry named on the declaration wins +// outright, and no scan happens. + +#include + +#include + +#define BOOST_TEST_MODULE adl_registry_scan +#include + +using namespace boost::openmethod; + +struct zoo_registry : default_registry::with {}; +struct other_registry : default_registry::with {}; + +namespace zoo { + +struct Animal { + virtual ~Animal() = default; +}; + +auto boost_openmethod_registry(Animal*) -> zoo_registry; + +struct Dog : Animal {}; + +} // namespace zoo + +struct Widget { + virtual ~Widget() = default; +}; + +using zoo::Animal, zoo::Dog; + +template +using scan = detail::method_registry; + +// One virtual parameter with an affinity, in either shape. +static_assert(std::is_same_v)>, zoo_registry>); +static_assert(std::is_same_v)>, zoo_registry>); +static_assert(std::is_same_v&)>, zoo_registry>); +static_assert( + std::is_same_v&)>, zoo_registry>); + +// The affinity is inherited, so a derived class carries it too. +static_assert(std::is_same_v)>, zoo_registry>); + +// No affinity anywhere: the macro default, exactly as before this feature. +static_assert( + std::is_same_v)>, default_registry>); +static_assert(std::is_same_v, default_registry>); + +// Mixing a class that has an affinity with one that has none: the one without +// yields. This is what keeps a first affinity from cascading errors. +static_assert(std::is_same_v< + scan, virtual_)>, + zoo_registry>); +static_assert(std::is_same_v< + scan, virtual_)>, + zoo_registry>); + +// Mixed shapes agreeing. +static_assert(std::is_same_v< + scan, virtual_)>, + zoo_registry>); + +// Non-virtual parameters are ignored. +static_assert(std::is_same_v< + scan, char*)>, zoo_registry>); + +// A registry named on the declaration wins, and the parameters are not +// consulted at all - the form that predates this feature. +BOOST_OPENMETHOD(ping, (virtual_), std::string, other_registry); + +static_assert(std::is_same_v< + BOOST_OPENMETHOD_TYPE( + ping, (virtual_), std::string, other_registry), + method< + BOOST_OPENMETHOD_ID(ping), std::string(virtual_), + other_registry>>); + +BOOST_AUTO_TEST_CASE(scan_is_compile_time_only) { + BOOST_TEST(true); +} diff --git a/test/test_adl_registry_smart_ptr.cpp b/test/test_adl_registry_smart_ptr.cpp new file mode 100644 index 00000000..7d66262a --- /dev/null +++ b/test/test_adl_registry_smart_ptr.cpp @@ -0,0 +1,79 @@ +// 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) + +// The affinity is found through a smart pointer, so the alias spellings and +// the `virtual_ptr` they stand for agree about the registry. + +#include +#include + +#include +#include +#include +#include +#include + +#include + +#define BOOST_TEST_MODULE adl_registry_smart_ptr +#include + +using namespace boost::openmethod; + +struct zoo_registry : default_registry::with {}; + +namespace zoo { + +struct Animal : boost::intrusive_ref_counter { + virtual ~Animal() = default; +}; + +auto boost_openmethod_registry(Animal*) -> zoo_registry; + +struct Dog : Animal {}; + +} // namespace zoo + +using zoo::Animal, zoo::Dog; + +// the anchor is the pointee, whatever the wrapper +static_assert( + std::is_same_v>, zoo_registry>); +static_assert( + std::is_same_v>, zoo_registry>); +static_assert(std::is_same_v< + default_registry_of>, zoo_registry>); +static_assert(std::is_same_v< + default_registry_of&>, zoo_registry>); + +// so the alias and the type it stands for are the same type +static_assert(std::is_same_v< + shared_virtual_ptr, + virtual_ptr, zoo_registry>>); +static_assert(std::is_same_v< + unique_virtual_ptr, + virtual_ptr, zoo_registry>>); +static_assert(std::is_same_v< + boost_intrusive_virtual_ptr, + virtual_ptr, zoo_registry>>); + +BOOST_OPENMETHOD_CLASSES(Animal, Dog, zoo_registry); + +BOOST_OPENMETHOD(name, (shared_virtual_ptr), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (shared_virtual_ptr), std::string) { + return "dog"; +} + +BOOST_AUTO_TEST_CASE(factories_need_no_registry_argument) { + initialize(); + + auto dog = make_shared_virtual(); + static_assert(std::is_same_v>); + BOOST_TEST(name(dog) == "dog"); + + auto owned = make_unique_virtual(); + static_assert(std::is_same_v>); +} diff --git a/test/test_adl_registry_static_rtti.cpp b/test/test_adl_registry_static_rtti.cpp new file mode 100644 index 00000000..e4f7a136 --- /dev/null +++ b/test/test_adl_registry_static_rtti.cpp @@ -0,0 +1,61 @@ +// 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) + +// The ADL twin of test_static_rtti.cpp: the same registry, selected by an +// affinity instead of by BOOST_OPENMETHOD_DEFAULT_REGISTRY. Worth its own test +// because a `static_rtti` registry has no `vptr` policy, so every `virtual_ptr` +// has to be created where the exact class is known. + +#include + +#include +#include +#include + +#define BOOST_TEST_MODULE adl_registry_static_rtti +#include + +using namespace boost::openmethod; + +struct static_registry : registry {}; + +namespace shapes { + +struct Shape { + friend auto boost_openmethod_registry(Shape*) -> static_registry; +}; + +struct Square : Shape {}; +struct Circle : Shape {}; + +} // namespace shapes + +using shapes::Shape, shapes::Square, shapes::Circle; + +static_assert(std::is_same_v, static_registry>); +static_assert( + std::is_same_v, virtual_ptr>); + +BOOST_OPENMETHOD_CLASSES(Shape, Square, Circle, static_registry); + +BOOST_OPENMETHOD(name, (virtual_ptr), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (virtual_ptr), std::string) { + return "square"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (virtual_ptr), std::string) { + return "circle"; +} + +BOOST_AUTO_TEST_CASE(affinity_works_without_a_vptr_policy) { + initialize(); + + Square square; + Circle circle; + + BOOST_TEST(name(final_virtual_ptr(square)) == "square"); + BOOST_TEST(name(final_virtual_ptr(circle)) == "circle"); +} diff --git a/test/test_adl_registry_two_registries.cpp b/test/test_adl_registry_two_registries.cpp new file mode 100644 index 00000000..d14ce80f --- /dev/null +++ b/test/test_adl_registry_two_registries.cpp @@ -0,0 +1,83 @@ +// 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) + +// Two hierarchies with different affinities in one translation unit. Neither +// method names a registry; each lands in its own, and the two initialize and +// dispatch independently. + +#include + +#include +#include + +#define BOOST_TEST_MODULE adl_registry_two_registries +#include + +using namespace boost::openmethod; + +struct zoo_registry : default_registry::with {}; +struct garage_registry : default_registry::with {}; + +namespace zoo { + +struct Animal { + virtual ~Animal() = default; + friend auto boost_openmethod_registry(Animal*) -> zoo_registry; +}; + +struct Dog : Animal {}; + +} // namespace zoo + +namespace garage { + +struct Vehicle { + virtual ~Vehicle() = default; + friend auto boost_openmethod_registry(Vehicle*) -> garage_registry; +}; + +struct Truck : Vehicle {}; + +} // namespace garage + +BOOST_OPENMETHOD_CLASSES(zoo::Animal, zoo::Dog, zoo_registry); +BOOST_OPENMETHOD_CLASSES(garage::Vehicle, garage::Truck, garage_registry); + +BOOST_OPENMETHOD(describe, (virtual_), std::string); +BOOST_OPENMETHOD(inspect, (virtual_), std::string); + +static_assert(std::is_same_v< + BOOST_OPENMETHOD_TYPE( + describe, (virtual_), std::string), + method< + BOOST_OPENMETHOD_ID(describe), + std::string(virtual_), zoo_registry>>); + +static_assert(std::is_same_v< + BOOST_OPENMETHOD_TYPE( + inspect, (virtual_), std::string), + method< + BOOST_OPENMETHOD_ID(inspect), + std::string(virtual_), + garage_registry>>); + +BOOST_OPENMETHOD_OVERRIDE(describe, (const zoo::Dog&), std::string) { + return "a dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(inspect, (const garage::Truck&), std::string) { + return "a truck"; +} + +BOOST_AUTO_TEST_CASE(two_registries_side_by_side) { + initialize(); + initialize(); + + zoo::Dog rex; + garage::Truck lorry; + + BOOST_TEST(describe(rex) == "a dog"); + BOOST_TEST(inspect(lorry) == "a truck"); +} From b4ffef983a683a5260aaea5453f59880596e4a48 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Tue, 1 Sep 2026 21:59:25 -0400 Subject: [PATCH 2/5] test: force the instantiation the registry-mismatch check needs compile_fail_adl_registry_declared_mismatch only declared the method. The "registry mismatch" static_assert is a fold in `method`'s class body, so it fires when the class is instantiated - and declaring the method is not enough. gcc and clang instantiate it anyway through the static registrar; MSVC does not, so the file compiled and the compile-fail test failed on every Windows job. Call the method in main(), the way compile_fail_virtual_ptr_different_registries.cpp already does. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QoMTqq3duJAptRAbCXgNh1 --- test/compile_fail_adl_registry_declared_mismatch.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/compile_fail_adl_registry_declared_mismatch.cpp b/test/compile_fail_adl_registry_declared_mismatch.cpp index 411e33ee..511da69f 100644 --- a/test/compile_fail_adl_registry_declared_mismatch.cpp +++ b/test/compile_fail_adl_registry_declared_mismatch.cpp @@ -26,4 +26,13 @@ struct Animal { 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; } From 22a08ccd2c7511bf3f20afd65bb7ecf8af876375 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Tue, 1 Sep 2026 22:33:12 -0400 Subject: [PATCH 3/5] rename default_registry_of to registry_affinity "Affinity" is the term the documentation uses for the relation, so the query that reads it back should carry it too. `default_registry_of` also read badly where it mattered most: "Registry defaults to the default registry of Class". `affine_registry` was the other candidate and is worse - it predicates "affine" of the registry, when it is the class that has the affinity, and "affine" reads as affine geometry in a library whose flagship example dispatches on matrix types. The concept is adjusted to match the name. Every class now *has* a registry affinity: a declared one if it declares `boost_openmethod_registry`, the default affinity otherwise. A declared affinity wins over a default one, which is the same rule as before - a method may mix a class that declares an affinity with one that does not - but stated without the awkward "no affinity, which is not the same as an affinity for the default registry". It also removes a corner that framing had: a class declared explicitly to the default registry is no longer a special case, it simply has the default affinity like any other. detail::affinity_of becomes declared_affinity, and no_affinity becomes default_affinity, so the internals read the same way as the prose. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QoMTqq3duJAptRAbCXgNh1 --- CLAUDE.md | 12 +-- .../ROOT/pages/registries_and_policies.adoc | 19 ++--- include/boost/openmethod/core.hpp | 78 +++++++++---------- include/boost/openmethod/inplace_vptr.hpp | 4 +- .../interop/boost_intrusive_ptr.hpp | 4 +- .../openmethod/interop/std_shared_ptr.hpp | 4 +- .../openmethod/interop/std_unique_ptr.hpp | 4 +- test/test_adl_registry.cpp | 6 +- test/test_adl_registry_hidden_friend.cpp | 4 +- test/test_adl_registry_inheritance.cpp | 12 +-- test/test_adl_registry_inplace.cpp | 6 +- test/test_adl_registry_scan.cpp | 8 +- test/test_adl_registry_smart_ptr.cpp | 8 +- test/test_adl_registry_static_rtti.cpp | 2 +- 14 files changed, 87 insertions(+), 84 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 29ef7ccf..06118c6e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -492,14 +492,16 @@ rules: A class can name its registry instead of the program naming one for every class: declare `auto boost_openmethod_registry(Class*) -> Registry;`, preferably as a hidden friend. The class -then has an *affinity* for that registry, inherited by its derived classes, and -`default_registry_of` reads it back. `virtual_ptr` and the smart pointer aliases default to it, +then *declares* an affinity for that registry, inherited by its derived classes, and +`registry_affinity` reads it back. `virtual_ptr` and the smart pointer aliases default to it, and a method declared without a registry argument takes the affinity its virtual parameters agree on (`detail::method_registry`, driven by `detail::param_affinity` / `agreed_affinity`). -Having *no* affinity is not the same as an affinity for the default registry: only the former -yields, which is what lets a method mix a class that has one with a class that has none. -`detail::affinity_of` is where that mapping happens; nothing public returns an affinity. +Every class has an affinity; one that declares none has the *default* affinity. Only a *declared* +affinity constrains a method, so a method may mix a class that declares one with a class that does +not - the latter yields. `detail::declared_affinity` maps the default registry to +`detail::default_affinity` to express that; `registry_affinity` itself is a query and always +answers. Two things deliberately do **not** participate, and both are documented as such: diff --git a/doc/modules/ROOT/pages/registries_and_policies.adoc b/doc/modules/ROOT/pages/registries_and_policies.adoc index 3f43481b..9144a371 100644 --- a/doc/modules/ROOT/pages/registries_and_policies.adoc +++ b/doc/modules/ROOT/pages/registries_and_policies.adoc @@ -63,8 +63,8 @@ it needs no definition: include::example$adl_registry.cpp[tag=affinity] ---- -The class then has an _affinity_ for that registry, and everything that mentions -the class finds it. A method declared without a registry argument takes the +The class then _declares_ an affinity for that registry, and everything that +mentions the class finds it. A method declared without a registry argument takes the affinity of its virtual parameters: [source,c++] @@ -85,11 +85,12 @@ class derived from it, because the derived-to-base pointer conversion makes the root's declaration viable. A declaration for a derived class is a better match, and wins. -A class that declares nothing has *no* affinity, which is not the same as an -affinity for the default registry. Only the first yields: a method may mix a -class that has an affinity with one that has none, and lands in the former's -registry. Two virtual parameters with *different* affinities are an error, as is -a registry named on the method that contradicts one of its parameters. +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. A hidden friend, as above, is the spelling to prefer. The declaration must precede every use of the class in a method, a `virtual_ptr` or a class @@ -97,10 +98,10 @@ registration - declaring it later makes the program ill-formed with no diagnostic required, and compilers disagree silently about which registry the earlier use got. Being part of the class, a hidden friend cannot be late. -WARNING: An affinity does not reach +WARNING: A declared affinity does not reach xref:reference:BOOST_OPENMETHOD_CLASSES.adoc[BOOST_OPENMETHOD_CLASSES], which still registers into `BOOST_OPENMETHOD_DEFAULT_REGISTRY` unless a registry is -listed last. Registering a class with an affinity, without naming its registry, +listed last. Registering a class that declares an affinity, without naming its registry, puts the class in one registry and its methods in another - and that shows up as a `missing_class` error at run time, not as a compile error. List the registry: `BOOST_OPENMETHOD_CLASSES(Animal, Dog, zoo_registry)`. diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index f3535079..2335e001 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -143,11 +143,11 @@ constexpr bool false_t = false; // workaround before CWG2518/P2593R1 //! //! This declaration is a catch-all that matches any argument list and returns //! @ref BOOST_OPENMETHOD_DEFAULT_REGISTRY, denoting the absence of -//! customization. If an overload beats it for a given class, that class has an -//! *affinity* for the overload's return type, and that registry becomes the -//! default for every construct that mentions the class: @ref virtual_ptr and -//! the smart pointer aliases, and the methods that take it as a virtual -//! parameter. +//! customization. If an overload beats it for a given class, that class +//! *declares* an affinity for the overload's return type, and that registry +//! becomes the default for every construct that mentions the class: @ref +//! virtual_ptr and the smart pointer aliases, and the methods that take it as a +//! virtual parameter. //! //! An affinity declared for a class extends to its derived classes, because //! the derived-to-base pointer conversion makes the base's overload viable. An @@ -174,7 +174,7 @@ constexpr bool false_t = false; // workaround before CWG2518/P2593R1 //! //! include:../examples/adl_registry.cpp#affinity //! -//! @see @ref default_registry_of +//! @see @ref registry_affinity //! @see [Registries and Policies](xref:ROOT:registries_and_policies.adoc) auto boost_openmethod_registry(...) -> BOOST_OPENMETHOD_DEFAULT_REGISTRY; @@ -230,29 +230,29 @@ using adl_registry = } // namespace detail -//! The registry a class belongs to by default. +//! The registry a class has an affinity for. //! -//! Evaluates to the return type of the @ref boost_openmethod_registry overload -//! found for `T` by argument-dependent lookup, after removing cv-qualifiers, -//! references and pointers, and unwrapping one level of smart pointer - so -//! `Class`, `const Class&`, `Class*` and `std::shared_ptr` all yield the -//! same registry. +//! Every class has a registry affinity. A class *declares* one with a @ref +//! boost_openmethod_registry overload; one that declares none has the *default* +//! affinity, @ref BOOST_OPENMETHOD_DEFAULT_REGISTRY. //! -//! If `T`\'s class has no affinity, this is @ref -//! BOOST_OPENMETHOD_DEFAULT_REGISTRY. Note that this is a query, and always -//! answers; having *no* affinity is not the same as having an affinity for the -//! default registry, a distinction that matters when a method draws its -//! registry from its virtual parameters. +//! Evaluates to the return type of the overload found for `T` by +//! argument-dependent lookup, after removing cv-qualifiers, references and +//! pointers, and unwrapping one level of smart pointer - so `Class`, `const +//! Class&`, `Class*` and `std::shared_ptr` all yield the same registry. +//! +//! A declared affinity wins over a default one. That is what lets a method mix +//! a class that declares an affinity with one that does not: the latter yields. //! //! @tparam T A class, or a reference, pointer or smart pointer to one. //! //! @see @ref boost_openmethod_registry //! @see [Registries and Policies](xref:ROOT:registries_and_policies.adoc) template -using default_registry_of = detail::adl_registry; +using registry_affinity = detail::adl_registry; template< - class Class, class Registry = default_registry_of, + class Class, class Registry = registry_affinity, typename = detail::sfinae> class virtual_ptr; @@ -943,7 +943,7 @@ inline auto final_virtual_ptr(Arg&& obj) { // doesn't like it. template inline auto final_virtual_ptr(Arg&& obj) { - return final_virtual_ptr, Arg>( + return final_virtual_ptr, Arg>( std::forward(obj)); } //! Wide pointer combining pointers to an object and its v-table @@ -1834,7 +1834,7 @@ class virtual_ptr< //! @return A `virtual_ptr`. template virtual_ptr(Class& obj) - -> virtual_ptr>; + -> virtual_ptr>; //! Construct a `virtual_ptr` from a xvalue reference. //! @@ -1843,7 +1843,7 @@ virtual_ptr(Class& obj) //! @return A `virtual_ptr`. template virtual_ptr(Class&& obj) - -> virtual_ptr>; + -> virtual_ptr>; // Alas this is not allowed: // template @@ -2179,57 +2179,57 @@ struct validate_method_parameter< //! namespace detail { -// A class that never declared `boost_openmethod_registry` has no affinity for -// any registry, which is not the same as an affinity for the default one. Only -// the former yields to a parameter that does have one, which is what lets a -// method mix a class that has an affinity with one that has none. -struct no_affinity; +// Every class has an affinity, but only a *declared* one constrains a method. +// A class that never declared `boost_openmethod_registry` has the default +// affinity, and yields to a parameter that declares one - which is what lets a +// method mix the two. +struct default_affinity; template -using affinity_of = std::conditional_t< - std::is_same_v, no_affinity, Registry>; +using declared_affinity = std::conditional_t< + std::is_same_v, default_affinity, Registry>; template struct param_affinity { - using type = no_affinity; + using type = default_affinity; }; template struct param_affinity> { - using type = affinity_of>; + using type = declared_affinity>; }; template struct param_affinity> { - using type = affinity_of; + using type = declared_affinity; }; template struct param_affinity&> { - using type = affinity_of; + using type = declared_affinity; }; template struct param_affinity&> { - using type = affinity_of; + using type = declared_affinity; }; // The first affinity in the parameter list wins; every other one must agree. template struct agreed_affinity { - using type = no_affinity; + using type = default_affinity; }; template struct agreed_affinity { using rest = typename agreed_affinity::type; static_assert( - std::is_same_v || - std::is_same_v || + std::is_same_v || + std::is_same_v || std::is_same_v, "virtual parameters have conflicting registry affinities"); using type = std:: - conditional_t, rest, Affinity>; + conditional_t, rest, Affinity>; }; // The registry a method takes when its declaration does not name one. @@ -2243,7 +2243,7 @@ struct method_registry_aux { using found = typename agreed_affinity< typename param_affinity::type...>::type; using type = std::conditional_t< - std::is_same_v, macro_default_registry, found>; + std::is_same_v, macro_default_registry, found>; }; template diff --git a/include/boost/openmethod/inplace_vptr.hpp b/include/boost/openmethod/inplace_vptr.hpp index 51b2a75b..d37d152a 100644 --- a/include/boost/openmethod/inplace_vptr.hpp +++ b/include/boost/openmethod/inplace_vptr.hpp @@ -12,12 +12,12 @@ namespace detail { void boost_openmethod_bases(...); -// `inplace_vptr_base` declares the affinity that `default_registry_of` reads +// `inplace_vptr_base` declares the affinity that `registry_affinity` reads // back, so the two are the same question. There is deliberately no catch-all // here: the one in core.hpp serves both, and a second one returning `void` // would shadow it for lookups from this namespace. template -using inplace_vptr_registry = default_registry_of; +using inplace_vptr_registry = registry_affinity; template struct update_vptr_bases; diff --git a/include/boost/openmethod/interop/boost_intrusive_ptr.hpp b/include/boost/openmethod/interop/boost_intrusive_ptr.hpp index 4e7e9c57..e530c9ee 100644 --- a/include/boost/openmethod/interop/boost_intrusive_ptr.hpp +++ b/include/boost/openmethod/interop/boost_intrusive_ptr.hpp @@ -129,7 +129,7 @@ struct virtual_traits&, Registry> { //! include:intrusive_ptr.cpp#boost_intrusive_virtual_ptr_alias //! //! @see [Smart Pointers](xref:ROOT:smart_pointers.adoc) -template> +template> using boost_intrusive_virtual_ptr = virtual_ptr, Registry>; @@ -153,7 +153,7 @@ using boost_intrusive_virtual_ptr = //! //! @see [Smart Pointers](xref:ROOT:smart_pointers.adoc) template< - class Class, class Registry = default_registry_of, + class Class, class Registry = registry_affinity, typename... T> inline auto make_boost_intrusive_virtual(T&&... args) { return final_virtual_ptr(intrusive_ptr( diff --git a/include/boost/openmethod/interop/std_shared_ptr.hpp b/include/boost/openmethod/interop/std_shared_ptr.hpp index 02b35306..799c835c 100644 --- a/include/boost/openmethod/interop/std_shared_ptr.hpp +++ b/include/boost/openmethod/interop/std_shared_ptr.hpp @@ -202,7 +202,7 @@ struct virtual_traits&, Registry> { //! include:smart_pointers.cpp#shared_virtual_ptr_alias //! //! @see [Smart Pointers](xref:ROOT:smart_pointers.adoc) -template> +template> using shared_virtual_ptr = virtual_ptr, Registry>; //! Create a new object and return a `shared_virtual_ptr` to it. @@ -225,7 +225,7 @@ using shared_virtual_ptr = virtual_ptr, Registry>; //! //! @see [Smart Pointers](xref:ROOT:smart_pointers.adoc) template< - class Class, class Registry = default_registry_of, + class Class, class Registry = registry_affinity, typename... T> inline auto make_shared_virtual(T&&... args) { return final_virtual_ptr( diff --git a/include/boost/openmethod/interop/std_unique_ptr.hpp b/include/boost/openmethod/interop/std_unique_ptr.hpp index b99a6e52..f816a336 100644 --- a/include/boost/openmethod/interop/std_unique_ptr.hpp +++ b/include/boost/openmethod/interop/std_unique_ptr.hpp @@ -72,7 +72,7 @@ struct virtual_traits, Registry> { //! include:smart_pointers.cpp#unique_virtual_ptr_alias //! //! @see [Smart Pointers](xref:ROOT:smart_pointers.adoc) -template> +template> using unique_virtual_ptr = virtual_ptr, Registry>; //! Create a new object and return a `unique_virtual_ptr` to it. @@ -95,7 +95,7 @@ using unique_virtual_ptr = virtual_ptr, Registry>; //! //! @see [Smart Pointers](xref:ROOT:smart_pointers.adoc) template< - class Class, class Registry = default_registry_of, + class Class, class Registry = registry_affinity, typename... T> inline auto make_unique_virtual(T&&... args) { return final_virtual_ptr( diff --git a/test/test_adl_registry.cpp b/test/test_adl_registry.cpp index 36861404..874b0186 100644 --- a/test/test_adl_registry.cpp +++ b/test/test_adl_registry.cpp @@ -39,9 +39,9 @@ struct Widget { using zoo::Animal, zoo::Dog, zoo::Cat; -static_assert(std::is_same_v, zoo_registry>); -static_assert(std::is_same_v, zoo_registry>); -static_assert(std::is_same_v, default_registry>); +static_assert(std::is_same_v, zoo_registry>); +static_assert(std::is_same_v, zoo_registry>); +static_assert(std::is_same_v, default_registry>); // `virtual_ptr` picks it up, so `virtual_ptr` is not a `virtual_ptr` in // the default registry. diff --git a/test/test_adl_registry_hidden_friend.cpp b/test/test_adl_registry_hidden_friend.cpp index 0efb6858..a7971f57 100644 --- a/test/test_adl_registry_hidden_friend.cpp +++ b/test/test_adl_registry_hidden_friend.cpp @@ -33,8 +33,8 @@ class Dog : public Animal {}; } // namespace zoo -static_assert(std::is_same_v, zoo_registry>); -static_assert(std::is_same_v, zoo_registry>); +static_assert(std::is_same_v, zoo_registry>); +static_assert(std::is_same_v, zoo_registry>); // The method lives in a third namespace, and still finds the affinity. namespace vet { diff --git a/test/test_adl_registry_inheritance.cpp b/test/test_adl_registry_inheritance.cpp index 1e57f7f0..5212b33d 100644 --- a/test/test_adl_registry_inheritance.cpp +++ b/test/test_adl_registry_inheritance.cpp @@ -44,22 +44,22 @@ struct Chimera : Left, Right {}; using namespace zoo; // inherited, however deep -static_assert(std::is_same_v, zoo_registry>); -static_assert(std::is_same_v, zoo_registry>); +static_assert(std::is_same_v, zoo_registry>); +static_assert(std::is_same_v, zoo_registry>); // the exact match wins, and is itself inherited -static_assert(std::is_same_v, kennel_registry>); -static_assert(std::is_same_v, kennel_registry>); +static_assert(std::is_same_v, kennel_registry>); +static_assert(std::is_same_v, kennel_registry>); // one Animal, so one affinity -static_assert(std::is_same_v, zoo_registry>); +static_assert(std::is_same_v, zoo_registry>); // An unrelated class is untouched by any of it. struct Widget { virtual ~Widget() = default; }; -static_assert(std::is_same_v, default_registry>); +static_assert(std::is_same_v, default_registry>); BOOST_AUTO_TEST_CASE(inheritance_is_compile_time_only) { BOOST_TEST(true); diff --git a/test/test_adl_registry_inplace.cpp b/test/test_adl_registry_inplace.cpp index 98281244..0436237e 100644 --- a/test/test_adl_registry_inplace.cpp +++ b/test/test_adl_registry_inplace.cpp @@ -27,9 +27,9 @@ struct Animal : bom::inplace_vptr_base {}; struct Dog : Animal, bom::inplace_vptr_derived {}; struct Cat : Animal, bom::inplace_vptr_derived {}; -// The mixin's hidden friend is what `default_registry_of` reads back. -static_assert(std::is_same_v, zoo_registry>); -static_assert(std::is_same_v, zoo_registry>); +// The mixin's hidden friend is what `registry_affinity` reads back. +static_assert(std::is_same_v, zoo_registry>); +static_assert(std::is_same_v, zoo_registry>); // No registry named, and no #define: the method follows the class. BOOST_OPENMETHOD(speak, (bom::virtual_), std::string); diff --git a/test/test_adl_registry_scan.cpp b/test/test_adl_registry_scan.cpp index 8e865c37..4f7e3789 100644 --- a/test/test_adl_registry_scan.cpp +++ b/test/test_adl_registry_scan.cpp @@ -4,9 +4,9 @@ // or copy at http://www.boost.org/LICENSE_1_0.txt) // How a method picks its registry: it takes the affinity its virtual -// parameters agree on. A class with no affinity contributes nothing, so it -// yields rather than conflicting. A registry named on the declaration wins -// outright, and no scan happens. +// parameters agree on. Only a *declared* affinity constrains it, so a class +// with the default affinity yields rather than conflicting. A registry named on +// the declaration wins outright, and no scan happens. #include @@ -56,7 +56,7 @@ static_assert( std::is_same_v)>, default_registry>); static_assert(std::is_same_v, default_registry>); -// Mixing a class that has an affinity with one that has none: the one without +// Mixing a class that declares an affinity with one that does not: the latter // yields. This is what keeps a first affinity from cascading errors. static_assert(std::is_same_v< scan, virtual_)>, diff --git a/test/test_adl_registry_smart_ptr.cpp b/test/test_adl_registry_smart_ptr.cpp index 7d66262a..c668abdd 100644 --- a/test/test_adl_registry_smart_ptr.cpp +++ b/test/test_adl_registry_smart_ptr.cpp @@ -40,13 +40,13 @@ using zoo::Animal, zoo::Dog; // the anchor is the pointee, whatever the wrapper static_assert( - std::is_same_v>, zoo_registry>); + std::is_same_v>, zoo_registry>); static_assert( - std::is_same_v>, zoo_registry>); + std::is_same_v>, zoo_registry>); static_assert(std::is_same_v< - default_registry_of>, zoo_registry>); + registry_affinity>, zoo_registry>); static_assert(std::is_same_v< - default_registry_of&>, zoo_registry>); + registry_affinity&>, zoo_registry>); // so the alias and the type it stands for are the same type static_assert(std::is_same_v< diff --git a/test/test_adl_registry_static_rtti.cpp b/test/test_adl_registry_static_rtti.cpp index e4f7a136..de29dfe2 100644 --- a/test/test_adl_registry_static_rtti.cpp +++ b/test/test_adl_registry_static_rtti.cpp @@ -34,7 +34,7 @@ struct Circle : Shape {}; using shapes::Shape, shapes::Square, shapes::Circle; -static_assert(std::is_same_v, static_registry>); +static_assert(std::is_same_v, static_registry>); static_assert( std::is_same_v, virtual_ptr>); From c03669be3b6405523e245a82dd9717b55096f656 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Wed, 2 Sep 2026 09:50:37 -0400 Subject: [PATCH 4/5] chore: conform to clang-format 22 develop adopted clang-format 22 in 6b02978; reflow the files this branch touches to match, so the diff carries no formatting noise. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QoMTqq3duJAptRAbCXgNh1 --- doc/modules/ROOT/examples/adl_registry.cpp | 6 +++--- include/boost/openmethod/core.hpp | 18 ++++++++---------- .../openmethod/interop/boost_intrusive_ptr.hpp | 4 +--- .../openmethod/interop/std_shared_ptr.hpp | 4 +--- .../openmethod/interop/std_unique_ptr.hpp | 4 +--- test/test_adl_registry.cpp | 11 ++++++----- test/test_adl_registry_inplace.cpp | 6 +++--- test/test_adl_registry_scan.cpp | 13 +++++++------ test/test_adl_registry_smart_ptr.cpp | 4 ++-- test/test_adl_registry_two_registries.cpp | 14 +++++++------- 10 files changed, 39 insertions(+), 45 deletions(-) diff --git a/doc/modules/ROOT/examples/adl_registry.cpp b/doc/modules/ROOT/examples/adl_registry.cpp index 03d6bcc1..feb3a888 100644 --- a/doc/modules/ROOT/examples/adl_registry.cpp +++ b/doc/modules/ROOT/examples/adl_registry.cpp @@ -12,9 +12,9 @@ #include // tag::registry[] -struct zoo_registry - : boost::openmethod::default_registry::with< - boost::openmethod::policies::runtime_checks> {}; +struct zoo_registry : + boost::openmethod::default_registry::with< + boost::openmethod::policies::runtime_checks> {}; // end::registry[] using namespace boost::openmethod; diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index 2335e001..15be9cff 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -224,9 +224,8 @@ using unadorned = std::remove_cv_t< std::remove_pointer_t>>>; template -using adl_registry = - typename adl_registry_aux>::type>>::type; +using adl_registry = typename adl_registry_aux< + unadorned>::type>>::type; } // namespace detail @@ -1833,8 +1832,7 @@ class virtual_ptr< //! @param obj A lvalue reference to an object. //! @return A `virtual_ptr`. template -virtual_ptr(Class& obj) - -> virtual_ptr>; +virtual_ptr(Class& obj) -> virtual_ptr>; //! Construct a `virtual_ptr` from a xvalue reference. //! @@ -1842,8 +1840,7 @@ virtual_ptr(Class& obj) //! @param obj A xvalue reference to an object. //! @return A `virtual_ptr`. template -virtual_ptr(Class&& obj) - -> virtual_ptr>; +virtual_ptr(Class&& obj) -> virtual_ptr>; // Alas this is not allowed: // template @@ -2187,7 +2184,8 @@ struct default_affinity; template using declared_affinity = std::conditional_t< - std::is_same_v, default_affinity, Registry>; + std::is_same_v, default_affinity, + Registry>; template struct param_affinity { @@ -2228,8 +2226,8 @@ struct agreed_affinity { std::is_same_v || std::is_same_v, "virtual parameters have conflicting registry affinities"); - using type = std:: - conditional_t, rest, Affinity>; + using type = std::conditional_t< + std::is_same_v, rest, Affinity>; }; // The registry a method takes when its declaration does not name one. diff --git a/include/boost/openmethod/interop/boost_intrusive_ptr.hpp b/include/boost/openmethod/interop/boost_intrusive_ptr.hpp index e530c9ee..16dd5b91 100644 --- a/include/boost/openmethod/interop/boost_intrusive_ptr.hpp +++ b/include/boost/openmethod/interop/boost_intrusive_ptr.hpp @@ -152,9 +152,7 @@ using boost_intrusive_virtual_ptr = //! include:intrusive_ptr.cpp#make_boost_intrusive_virtual //! //! @see [Smart Pointers](xref:ROOT:smart_pointers.adoc) -template< - class Class, class Registry = registry_affinity, - typename... T> +template, typename... T> inline auto make_boost_intrusive_virtual(T&&... args) { return final_virtual_ptr(intrusive_ptr( new std::remove_cv_t(std::forward(args)...))); diff --git a/include/boost/openmethod/interop/std_shared_ptr.hpp b/include/boost/openmethod/interop/std_shared_ptr.hpp index 799c835c..4c9b67cb 100644 --- a/include/boost/openmethod/interop/std_shared_ptr.hpp +++ b/include/boost/openmethod/interop/std_shared_ptr.hpp @@ -224,9 +224,7 @@ using shared_virtual_ptr = virtual_ptr, Registry>; //! include:smart_pointers.cpp#make_shared_virtual //! //! @see [Smart Pointers](xref:ROOT:smart_pointers.adoc) -template< - class Class, class Registry = registry_affinity, - typename... T> +template, typename... T> inline auto make_shared_virtual(T&&... args) { return final_virtual_ptr( std::make_shared(std::forward(args)...)); diff --git a/include/boost/openmethod/interop/std_unique_ptr.hpp b/include/boost/openmethod/interop/std_unique_ptr.hpp index f816a336..30f21571 100644 --- a/include/boost/openmethod/interop/std_unique_ptr.hpp +++ b/include/boost/openmethod/interop/std_unique_ptr.hpp @@ -94,9 +94,7 @@ using unique_virtual_ptr = virtual_ptr, Registry>; //! include:smart_pointers.cpp#make_unique_virtual //! //! @see [Smart Pointers](xref:ROOT:smart_pointers.adoc) -template< - class Class, class Registry = registry_affinity, - typename... T> +template, typename... T> inline auto make_unique_virtual(T&&... args) { return final_virtual_ptr( std::make_unique(std::forward(args)...)); diff --git a/test/test_adl_registry.cpp b/test/test_adl_registry.cpp index 874b0186..edcef792 100644 --- a/test/test_adl_registry.cpp +++ b/test/test_adl_registry.cpp @@ -55,11 +55,12 @@ BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat, zoo_registry); BOOST_OPENMETHOD(speak, (virtual_), std::string); BOOST_OPENMETHOD(poke, (virtual_ptr), std::string); -static_assert(std::is_same_v< - BOOST_OPENMETHOD_TYPE(speak, (virtual_), std::string), - method< - BOOST_OPENMETHOD_ID(speak), std::string(virtual_), - zoo_registry>>); +static_assert( + std::is_same_v< + BOOST_OPENMETHOD_TYPE(speak, (virtual_), std::string), + method< + BOOST_OPENMETHOD_ID(speak), std::string(virtual_), + zoo_registry>>); BOOST_OPENMETHOD_OVERRIDE(speak, (const Dog&), std::string) { return "bark"; diff --git a/test/test_adl_registry_inplace.cpp b/test/test_adl_registry_inplace.cpp index 0436237e..1c6b4d92 100644 --- a/test/test_adl_registry_inplace.cpp +++ b/test/test_adl_registry_inplace.cpp @@ -19,9 +19,9 @@ namespace bom = boost::openmethod; // An inplace_vptr hierarchy needs neither a vptr policy nor a type hash. -struct zoo_registry - : bom::default_registry::without { -}; +struct zoo_registry : + bom::default_registry::without< + bom::policies::vptr, bom::policies::type_hash> {}; struct Animal : bom::inplace_vptr_base {}; struct Dog : Animal, bom::inplace_vptr_derived {}; diff --git a/test/test_adl_registry_scan.cpp b/test/test_adl_registry_scan.cpp index 4f7e3789..e8e764e8 100644 --- a/test/test_adl_registry_scan.cpp +++ b/test/test_adl_registry_scan.cpp @@ -42,7 +42,8 @@ template using scan = detail::method_registry; // One virtual parameter with an affinity, in either shape. -static_assert(std::is_same_v)>, zoo_registry>); +static_assert( + std::is_same_v)>, zoo_registry>); static_assert(std::is_same_v)>, zoo_registry>); static_assert(std::is_same_v&)>, zoo_registry>); static_assert( @@ -66,9 +67,9 @@ static_assert(std::is_same_v< zoo_registry>); // Mixed shapes agreeing. -static_assert(std::is_same_v< - scan, virtual_)>, - zoo_registry>); +static_assert( + std::is_same_v< + scan, virtual_)>, zoo_registry>); // Non-virtual parameters are ignored. static_assert(std::is_same_v< @@ -82,8 +83,8 @@ static_assert(std::is_same_v< BOOST_OPENMETHOD_TYPE( ping, (virtual_), std::string, other_registry), method< - BOOST_OPENMETHOD_ID(ping), std::string(virtual_), - other_registry>>); + BOOST_OPENMETHOD_ID(ping), + std::string(virtual_), other_registry>>); BOOST_AUTO_TEST_CASE(scan_is_compile_time_only) { BOOST_TEST(true); diff --git a/test/test_adl_registry_smart_ptr.cpp b/test/test_adl_registry_smart_ptr.cpp index c668abdd..2137433f 100644 --- a/test/test_adl_registry_smart_ptr.cpp +++ b/test/test_adl_registry_smart_ptr.cpp @@ -43,8 +43,8 @@ static_assert( std::is_same_v>, zoo_registry>); static_assert( std::is_same_v>, zoo_registry>); -static_assert(std::is_same_v< - registry_affinity>, zoo_registry>); +static_assert( + std::is_same_v>, zoo_registry>); static_assert(std::is_same_v< registry_affinity&>, zoo_registry>); diff --git a/test/test_adl_registry_two_registries.cpp b/test/test_adl_registry_two_registries.cpp index d14ce80f..66ffd276 100644 --- a/test/test_adl_registry_two_registries.cpp +++ b/test/test_adl_registry_two_registries.cpp @@ -55,13 +55,13 @@ static_assert(std::is_same_v< BOOST_OPENMETHOD_ID(describe), std::string(virtual_), zoo_registry>>); -static_assert(std::is_same_v< - BOOST_OPENMETHOD_TYPE( - inspect, (virtual_), std::string), - method< - BOOST_OPENMETHOD_ID(inspect), - std::string(virtual_), - garage_registry>>); +static_assert( + std::is_same_v< + BOOST_OPENMETHOD_TYPE( + inspect, (virtual_), std::string), + method< + BOOST_OPENMETHOD_ID(inspect), + std::string(virtual_), garage_registry>>); BOOST_OPENMETHOD_OVERRIDE(describe, (const zoo::Dog&), std::string) { return "a dog"; From ab876ed0b0e5f65c32ea8e0f4716a286c4e07b17 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Mon, 14 Sep 2026 21:47:28 -0400 Subject: [PATCH 5/5] registry affinity: a member typedef, and five fixes to how it is answered Review of this branch turned up five ways `registry_affinity` gives a wrong or unchecked answer, each reproduced on gcc 13, clang 18 and MSVC. **Memoization.** `registry_affinity` is a class template specialization: asked once, then remembered. A class mentioned before it is complete - a `virtual_ptr` named after `class Cat;`, or `virtual_ptr` as a member of `Node` - is asked before its base classes, or a declaration further down its body, can be seen. The answer, the default registry, then stuck for the whole translation unit: a method over the class landed in one registry while `BOOST_OPENMETHOD_CLASSES(..., zoo_registry)` registered it in another, and two translation units that completed the class in different orders saw two `virtual_ptr` types - `nm` showed `U feed(virtual_ptr)` in one object against `T feed(virtual_ptr)` in the other. The doc said the opposite: "Being part of the class, a hidden friend cannot be late." Refusing to answer an incomplete class is not open to us: `virtual_ptr next;` in a plain linked structure declares no affinity, needs none, and has worked since #109. So every question now carries a tag. The `asked` one builds types, as before; `check_affinity` puts the same question again under `rechecked` where the class must be complete anyway - a virtual parameter, in `validate_method_parameter`, and a registration, in `use_class_aux` - and refuses an answer that has changed. Declaring nothing stays silent; declaring too late is an error, reported where the wrong registry would do its damage rather than at the mention, where nothing is wrong yet. **The anchor.** `registry_anchor` unwrapped anything with a nested `element_type`, so a polymorphic class that happens to define one - a matrix, a buffer - lost its own declared affinity; an `inplace_vptr` class with one registered into the wrong registry and stored that registry's null `static_vptr` in the object. A smart pointer is now a type `virtual_traits` is specialized for: `detail::virtual_type`, no `virtual_type` in the tree depending on the registry. **The return type** was taken verbatim: `-> const zoo_registry` yielded a distinct registry, with its own `registry_state`, disjoint from the one the classes were registered in; `-> int` failed at the first `sizeof(virtual_ptr)`, far from the declaration. It is stripped of cv-qualifiers and checked with `is_registry`. **The sentinel.** The catch-all returned `BOOST_OPENMETHOD_DEFAULT_REGISTRY` itself, so "no opinion" had to be recovered by comparing with the macro. An affinity explicitly declared for the default registry was therefore treated as none and yielded in a mixed method, and a registry spelled on a `virtual_ptr` parameter counted as a declared affinity - so a method over `virtual_ptr` naming no registry, formerly the `registry mismatch` error, silently landed in `other_registry`. The catch-all now returns `detail::default_affinity`. `declared` is the raw answer and constrains a method; `registry_affinity` maps the sentinel to the macro default and stays a query that always answers a registry. A `virtual_ptr` parameter contributes its class's affinity, never the registry it spells. **The mismatch check** covered only a by-value `virtual_ptr`: `virtual_` and `const virtual_ptr&` contradictions compiled clean. All four shapes are checked now. Along the way, a class can declare its affinity with `using boost_openmethod_registry = R;`. Member lookup finds it: inherited, hidden by a derived class's own, ambiguous between two bases that disagree. It takes precedence over the overload, and being visible from the point it is declared, it is the spelling for a class that mentions `virtual_ptr` of itself in its own body - which has already decided to be openmethod-aware, so the typedef intrudes no further. `inplace_vptr_base` provides it. It also sidesteps the drawbacks of a free function found only by ADL: a wrong namespace, a translation unit that does not see it, a function template that outranks it. Nine compile-fail tests, one per diagnosis. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RQG6CbE4o2agseE7bDVzHS --- CLAUDE.md | 29 +- doc/modules/ROOT/examples/adl_registry.cpp | 15 + .../ROOT/pages/registries_and_policies.adoc | 34 +- include/boost/openmethod/core.hpp | 326 +++++++++++++----- include/boost/openmethod/inplace_vptr.hpp | 7 + ...ile_fail_adl_registry_incomplete_class.cpp | 35 ++ ...pile_fail_adl_registry_incomplete_self.cpp | 36 ++ ...mpile_fail_adl_registry_not_a_registry.cpp | 21 ++ ...e_fail_adl_registry_parameter_registry.cpp | 32 ++ ...mpile_fail_adl_registry_pinned_default.cpp | 37 ++ ...compile_fail_adl_registry_ref_mismatch.cpp | 36 ++ ...ile_fail_adl_registry_virtual_mismatch.cpp | 34 ++ test/test_adl_registry.cpp | 60 ++++ test/test_adl_registry_scan.cpp | 22 ++ 14 files changed, 630 insertions(+), 94 deletions(-) create mode 100644 test/compile_fail_adl_registry_incomplete_class.cpp create mode 100644 test/compile_fail_adl_registry_incomplete_self.cpp create mode 100644 test/compile_fail_adl_registry_not_a_registry.cpp create mode 100644 test/compile_fail_adl_registry_parameter_registry.cpp create mode 100644 test/compile_fail_adl_registry_pinned_default.cpp create mode 100644 test/compile_fail_adl_registry_ref_mismatch.cpp create mode 100644 test/compile_fail_adl_registry_virtual_mismatch.cpp diff --git a/CLAUDE.md b/CLAUDE.md index 06118c6e..6013f039 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -499,9 +499,32 @@ on (`detail::method_registry`, driven by `detail::param_affinity` / `agreed_affi Every class has an affinity; one that declares none has the *default* affinity. Only a *declared* affinity constrains a method, so a method may mix a class that declares one with a class that does -not - the latter yields. `detail::declared_affinity` maps the default registry to -`detail::default_affinity` to express that; `registry_affinity` itself is a query and always -answers. +not - the latter yields. The catch-all `boost_openmethod_registry(...)` returns the sentinel +`detail::default_affinity`, not a registry, so an affinity declared for the default registry +itself still counts as declared; `detail::registry_affinity_aux::declared` is the raw answer +(sentinel or registry) and `registry_affinity` the query, which always answers a registry. A +`virtual_ptr` parameter contributes its *class's* affinity, never the registry it spells; a +spelled registry must agree with the class (`validate_method_parameter`, all three shapes). + +Two spellings, looked up in this order: a member typedef `boost_openmethod_registry` +(`detail::member_registry_aux` - ordinary member lookup, so inherited, hidden by a derived class's +own, ambiguous between two bases) and the ADL overload. `inplace_vptr_base` provides the typedef. + +**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 +asked before a declaration further down its body can be seen, and an untagged class template +specialization would remember that answer for the whole TU. Refusing to answer is not an option: +`virtual_ptr next;` in a plain linked structure declares no affinity and must keep working +(`test_virtual_ptr_self_referential.cpp`, on develop since #109). So `declared_affinity_aux` is the answer that builds types, and `check_affinity` puts the same question again +under the `rechecked` tag at the points where the class must be complete anyway - a virtual +parameter in `validate_method_parameter`, a registration in `use_class_aux` - and refuses an +answer that has changed. That is where a wrong registry would do its damage. The ambiguous-bases +diagnosis in `adl_affinity` is guarded on the tag, or the recheck would repeat it. + +Anchoring goes through `virtual_traits` (`detail::virtual_type`), never +a bare `element_type` probe: a polymorphic class may define `element_type` and must keep its own +affinity. Two things deliberately do **not** participate, and both are documented as such: diff --git a/doc/modules/ROOT/examples/adl_registry.cpp b/doc/modules/ROOT/examples/adl_registry.cpp index feb3a888..ff83af45 100644 --- a/doc/modules/ROOT/examples/adl_registry.cpp +++ b/doc/modules/ROOT/examples/adl_registry.cpp @@ -58,6 +58,21 @@ static_assert( std::is_same_v, virtual_ptr>); // end::virtual_ptr[] +// tag::typedef[] +namespace zoo { + +struct Cage { + using boost_openmethod_registry = zoo_registry; + + virtual ~Cage() = default; + virtual_ptr next; +}; + +} // namespace zoo +// end::typedef[] + +static_assert(std::is_same_v, zoo_registry>); + BOOST_AUTO_TEST_CASE(adl_registry) { // tag::call[] initialize(); diff --git a/doc/modules/ROOT/pages/registries_and_policies.adoc b/doc/modules/ROOT/pages/registries_and_policies.adoc index 9144a371..5795cdaa 100644 --- a/doc/modules/ROOT/pages/registries_and_policies.adoc +++ b/doc/modules/ROOT/pages/registries_and_policies.adoc @@ -90,13 +90,35 @@ _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. +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. -A hidden friend, as above, is the spelling to prefer. The declaration must -precede every use of the class in a method, a `virtual_ptr` or a class -registration - declaring it later makes the program ill-formed with no -diagnostic required, and compilers disagree silently about which registry the -earlier use got. Being part of the class, a hidden friend cannot be late. +A class can also declare its affinity with a member typedef: + +[source,c++] +---- +include::example$adl_registry.cpp[tag=typedef] +---- + +The typedef takes precedence over the function, and is inherited like any +member - a derived class's typedef hides the base's. It is the spelling for a +class that mentions `virtual_ptr` of itself in its own body, where the class is +still incomplete: the typedef is visible from the point it is declared. Such a +class has already decided to be openmethod-aware; the typedef intrudes no +further. + +Either way, the declaration must precede the first `virtual_ptr` of the class, +or method taking it as a virtual parameter, that does not name a registry. The +answer is remembered for the rest of the translation unit, and a class +mentioned before it is complete - as a member of itself, or through a forward +declaration - is asked before a declaration further down can be seen. + +Declaring none is fine, and is the common case: a linked structure needs no +affinity, and `virtual_ptr next;` inside `Node` gets the default +registry, as it always has. Declaring one too late is an error - not at the +mention, where nothing is wrong yet, but where the class is complete and its +affinity matters: a virtual parameter of a method, or a class registration. WARNING: A declared affinity does not reach xref:reference:BOOST_OPENMETHOD_CLASSES.adoc[BOOST_OPENMETHOD_CLASSES], which diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index 15be9cff..b7d5920d 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -139,20 +139,39 @@ constexpr bool false_t = false; // workaround before CWG2518/P2593R1 } // namespace detail +namespace detail { + +// What the catch-all below returns: the absence of a declaration. Not a +// registry, so that an affinity declared for the default registry itself is +// still a *declared* one, and constrains a method like any other. +struct default_affinity {}; + +template +struct registry_affinity_aux; + +} // namespace detail + //! Return the registry a class belongs to (ADL customization point). //! -//! This declaration is a catch-all that matches any argument list and returns -//! @ref BOOST_OPENMETHOD_DEFAULT_REGISTRY, denoting the absence of -//! customization. If an overload beats it for a given class, that class -//! *declares* an affinity for the overload's return type, and that registry -//! becomes the default for every construct that mentions the class: @ref -//! virtual_ptr and the smart pointer aliases, and the methods that take it as a -//! virtual parameter. +//! This declaration is a catch-all that matches any argument list. It returns a +//! placeholder that denotes the absence of customization, which @ref +//! registry_affinity reads as @ref BOOST_OPENMETHOD_DEFAULT_REGISTRY. If an +//! overload beats it for a given class, that class *declares* an affinity for +//! the overload's return type, and that registry becomes the default for every +//! construct that mentions the class: @ref virtual_ptr and the smart pointer +//! aliases, and the methods that take it as a virtual parameter. //! //! An affinity declared for a class extends to its derived classes, because //! the derived-to-base pointer conversion makes the base's overload viable. An //! overload on the derived class itself is a better match, and wins. //! +//! A class can also declare its affinity with a member typedef, `using +//! boost_openmethod_registry = Registry;`. It is looked up first, and it is +//! inherited like any member - a derived class's typedef hides the base's. +//! Being visible from the point it is declared, it is the spelling for a class +//! that mentions `virtual_ptr` of itself in its own body - which a class that +//! declares no affinity may do freely. +//! //! @par Requirements //! //! The library uses argument-dependent lookup to find an overload that @@ -162,13 +181,17 @@ constexpr bool false_t = false; // workaround before CWG2518/P2593R1 //! class to the overload. It must not be dereferenced - the function is never //! called, only its return type is used. //! -//! @li The return type is a @ref registry. +//! @li The return type is a @ref registry, possibly cv-qualified. //! -//! The overload must be declared before the first construct that mentions the -//! class. Declaring it afterwards makes the program ill-formed, no diagnostic -//! required - compilers disagree silently on which registry the earlier -//! construct used. Declaring it as a hidden friend, as below, makes that -//! impossible. +//! The declaration must precede the first construct that mentions the class +//! without naming a registry: a `virtual_ptr` of it, or a method that takes it +//! as a virtual parameter. The answer is remembered for the rest of the +//! translation unit, and a class mentioned before it is complete - as a member +//! of itself, or through a forward declaration - is asked before a declaration +//! further down can be seen. Declaring none is fine, and is the common case; +//! declaring one too late is an error, reported where the class is complete +//! and its affinity matters: a virtual parameter of a method, or a class +//! registration. //! //! @par Example //! @@ -176,69 +199,25 @@ constexpr bool false_t = false; // workaround before CWG2518/P2593R1 //! //! @see @ref registry_affinity //! @see [Registries and Policies](xref:ROOT:registries_and_policies.adoc) -auto boost_openmethod_registry(...) -> BOOST_OPENMETHOD_DEFAULT_REGISTRY; - -namespace detail { - -// The catch-all is viable for every class, so the primary template is reached -// only when the call to `boost_openmethod_registry` is ill-formed rather than -// unmatched. Three ways to get there, all of them a base class whose overload -// cannot be used: two base classes with different affinities (ambiguous -// overload), a private base (inaccessible conversion), and a repeated -// non-virtual base (ambiguous conversion). Declaring the overload for the -// class itself resolves all three. -template -struct adl_registry_aux { - static_assert( - false_t, - "cannot tell which registry this class belongs to: " - "boost_openmethod_registry is ambiguous or inaccessible for it - " - "declare one for the class itself"); - using type = macro_default_registry; -}; - -template -struct adl_registry_aux< - Class, - std::void_t()))>> { - using type = decltype(boost_openmethod_registry(std::declval())); -}; - -// The type whose namespace and base classes are consulted. `virtual_traits::virtual_type` would be the exact answer, but it needs the very -// `Registry` being computed. Probing for `element_type` covers `std:: -// shared_ptr`, `std::unique_ptr` and `boost::intrusive_ptr` without depending -// on their headers. -template -struct registry_anchor { - using type = T; -}; - -template -struct registry_anchor> { - using type = typename T::element_type; -}; - -template -using unadorned = std::remove_cv_t< - std::remove_pointer_t>>>; - -template -using adl_registry = typename adl_registry_aux< - unadorned>::type>>::type; - -} // namespace detail +auto boost_openmethod_registry(...) -> detail::default_affinity; //! The registry a class has an affinity for. //! -//! Every class has a registry affinity. A class *declares* one with a @ref -//! boost_openmethod_registry overload; one that declares none has the *default* -//! affinity, @ref BOOST_OPENMETHOD_DEFAULT_REGISTRY. +//! Every class has a registry affinity. A class *declares* one with a member +//! typedef named `boost_openmethod_registry`, or with a @ref +//! boost_openmethod_registry overload - the typedef takes precedence; one that +//! declares none has the *default* affinity, @ref +//! BOOST_OPENMETHOD_DEFAULT_REGISTRY. //! //! Evaluates to the return type of the overload found for `T` by -//! argument-dependent lookup, after removing cv-qualifiers, references and -//! pointers, and unwrapping one level of smart pointer - so `Class`, `const -//! Class&`, `Class*` and `std::shared_ptr` all yield the same registry. +//! argument-dependent lookup, minus cv-qualifiers. `T` itself is first stripped +//! of cv-qualifiers, references and pointers, and a smart pointer - a type +//! that @ref virtual_traits is specialized for - is unwrapped, so `Class`, +//! `const Class&`, `Class*` and `std::shared_ptr` all yield the same +//! registry. +//! +//! The declaration must precede the first mention of the class: see @ref +//! boost_openmethod_registry. //! //! A declared affinity wins over a default one. That is what lets a method mix //! a class that declares an affinity with one that does not: the latter yields. @@ -248,7 +227,7 @@ using adl_registry = typename adl_registry_aux< //! @see @ref boost_openmethod_registry //! @see [Registries and Policies](xref:ROOT:registries_and_policies.adoc) template -using registry_affinity = detail::adl_registry; +using registry_affinity = typename detail::registry_affinity_aux::type; template< class Class, class Registry = registry_affinity, @@ -385,6 +364,137 @@ template using virtual_types = boost::mp11::mp_transform< remove_virtual_, boost::mp11::mp_filter>; +// ----------------------------------------------------------------------------- +// registry affinity + +// Every question below is tagged, because the answer to an untagged one would +// be memoized: a class template specialization is instantiated once per class +// and then remembered. `asked` is the question whose answer builds types - the +// registry of a `virtual_ptr`, of a method. `rechecked` is the same question, +// put again by `check_affinity` at a point where the class is complete, to +// catch a declaration that came too late to have been seen the first time. +struct asked; +struct rechecked; + +// The member typedef, looked up first. Ordinary member lookup: inherited, a +// derived class's hides the base's, and two bases that disagree are ambiguous +// - a substitution failure here, which the ADL path then reports. Inside the +// class's own body it is visible as soon as it is declared, which is what lets +// a class mention `virtual_ptr` of itself. +template +struct member_affinity { + using type = default_affinity; +}; + +template +struct member_affinity< + Class, Question, std::void_t> { + using type = std::remove_cv_t; +}; + +// The catch-all is viable for every class, so the primary template is reached +// only when the call to `boost_openmethod_registry` is ill-formed rather than +// unmatched. Three ways to get there, all of them a base class whose overload +// cannot be used: two base classes with different affinities (ambiguous +// overload), a private base (inaccessible conversion), and a repeated +// non-virtual base (ambiguous conversion). Declaring the overload for the +// class itself resolves all three. +template +struct adl_affinity { + // Only the question that builds types reports this. The recheck asks the + // same thing about the same class, and would say it twice. + static_assert( + !std::is_same_v, + "cannot tell which registry this class belongs to: " + "boost_openmethod_registry is ambiguous or inaccessible for it - " + "declare one for the class itself"); + using type = default_affinity; +}; + +template +struct adl_affinity< + Class, Question, + std::void_t()))>> { + using type = std::remove_cv_t()))>; +}; + +// What the class says: a registry, or `default_affinity` when it says nothing. +template +struct declared_affinity_aux { + // conditional_t picks the struct, so the ADL path is not instantiated + // when the typedef answers: its ambiguity diagnosis would fire for a class + // whose typedef settles what two base classes dispute. + using type = typename std::conditional_t< + std::is_same_v< + typename member_affinity::type, default_affinity>, + adl_affinity, member_affinity>::type; +}; + +template +using declared_affinity = typename declared_affinity_aux::type; + +// The type whose namespace and base classes are consulted: the class a smart +// pointer points to, as `virtual_traits` sees it, or `T` itself. No +// `virtual_type` depends on the registry, so any registry serves to ask. +template +struct registry_anchor { + using type = T; +}; + +template +struct registry_anchor< + T, + std::enable_if_t< + !std::is_void_v>>> { + using type = virtual_type; +}; + +template +using unadorned = std::remove_cv_t< + std::remove_pointer_t>>>; + +// `declared` is what the class says - and is what constrains a method. `type` +// is the query's answer, always a registry. +template +struct registry_affinity_aux { + using anchor = unadorned>::type>; + using declared = declared_affinity; + + static_assert( + std::is_same_v || is_registry, + "boost_openmethod_registry must return a registry"); + + using type = std::conditional_t< + std::is_same_v, macro_default_registry, + declared>; +}; + +// The answer above is remembered for the rest of the translation unit, and a +// class mentioned before it is complete - a `virtual_ptr` of it in a friend +// declaration, or as a member of the class itself - is asked before a +// declaration further down can be seen. Answering "none" then is right for a +// class that declares none, and that is the common case: a plain linked +// structure, `virtual_ptr next;`, needs no affinity and gets the default +// registry. It is wrong for one that declares later, and silently so. +// +// So ask again wherever the class must be complete anyway - a virtual +// parameter of a method, a class registration - and refuse an answer that has +// changed. That is where a wrong registry would do its damage, and by then a +// declaration in the class, or inherited from a base, is in place. +template +struct check_affinity { + static_assert( + std::is_same_v< + typename declared_affinity_aux::type, + declared_affinity>, + "this class declared its registry affinity after it was first " + "mentioned - a virtual_ptr of it, or a method over it, was formed " + "before the declaration and kept the wrong registry: move the " + "declaration above the first mention"); + static constexpr bool value = true; +}; + } // namespace detail BOOST_OPENMETHOD_OPEN_NAMESPACE_DETAIL_UNLESS_MRDOCS @@ -545,6 +655,11 @@ struct use_class_aux> : std::conditional_t< Registry::has_deferred_static_rtti, detail::deferred_class_info, detail::class_info> { + // A registration is a checkpoint: the class is complete here, so ask its + // affinity again and refuse one declared after the class was first + // mentioned. + static_assert(check_affinity::value); + static type_id bases[sizeof...(Bases)]; use_class_aux() { this->first_base = bases; @@ -2084,11 +2199,30 @@ struct validate_method_parameter< validate_method_parameter::value, "virtual_<> parameter is not a polymorphic class and no " "boost_openmethod_vptr is applicable"); + + // 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); + + // And a method that names a registry may not contradict it. + static_assert( + std::is_same_v< + typename registry_affinity_aux::declared, default_affinity> || + std::is_same_v< + typename registry_affinity_aux::declared, Registry>, + "registry mismatch: the class declares an affinity for another " + "registry"); }; +// 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 {}; + std::true_type { + static_assert(check_affinity::value); +}; template struct validate_method_parameter< @@ -2096,6 +2230,33 @@ struct validate_method_parameter< static_assert( false_t, "registry mismatch"); }; + +template +struct validate_method_parameter< + virtual_ptr&, 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 +struct validate_method_parameter< + const virtual_ptr&, Registry, void> : std::true_type { + static_assert(check_affinity::value); +}; + +template +struct validate_method_parameter< + const virtual_ptr&, MethodRegistry, void> : + std::false_type { + static_assert( + false_t, "registry mismatch"); +}; } // namespace detail //! Implement a method @@ -2179,14 +2340,9 @@ namespace detail { // Every class has an affinity, but only a *declared* one constrains a method. // A class that never declared `boost_openmethod_registry` has the default // affinity, and yields to a parameter that declares one - which is what lets a -// method mix the two. -struct default_affinity; - -template -using declared_affinity = std::conditional_t< - std::is_same_v, default_affinity, - Registry>; - +// method mix the two. A `virtual_ptr` parameter contributes its class's +// affinity, not the registry it names: the class decides, and a registry +// spelled on the parameter has to agree with it (validate_method_parameter). template struct param_affinity { using type = default_affinity; @@ -2194,22 +2350,22 @@ struct param_affinity { template struct param_affinity> { - using type = declared_affinity>; + using type = typename registry_affinity_aux::declared; }; template struct param_affinity> { - using type = declared_affinity; + using type = typename registry_affinity_aux::declared; }; template struct param_affinity&> { - using type = declared_affinity; + using type = typename registry_affinity_aux::declared; }; template struct param_affinity&> { - using type = declared_affinity; + using type = typename registry_affinity_aux::declared; }; // The first affinity in the parameter list wins; every other one must agree. diff --git a/include/boost/openmethod/inplace_vptr.hpp b/include/boost/openmethod/inplace_vptr.hpp index d37d152a..4dfbfe9d 100644 --- a/include/boost/openmethod/inplace_vptr.hpp +++ b/include/boost/openmethod/inplace_vptr.hpp @@ -99,6 +99,13 @@ class inplace_vptr_base_tag {}; //! @see [Virtual Pointer Alternatives](xref:ROOT:virtual_ptr_alt.adoc) template class inplace_vptr_base : protected detail::inplace_vptr_base_tag { + public: + //! The registry `Class` has an affinity for, i.e. `Registry`. Read back by + //! @ref registry_affinity, and inherited by the classes derived from + //! `Class` - inside their own bodies too. + using boost_openmethod_registry = Registry; + + private: template friend void detail::boost_openmethod_update_vptr(Other*); friend auto boost_openmethod_registry(Class*) -> Registry; diff --git a/test/compile_fail_adl_registry_incomplete_class.cpp b/test/compile_fail_adl_registry_incomplete_class.cpp new file mode 100644 index 00000000..395da112 --- /dev/null +++ b/test/compile_fail_adl_registry_incomplete_class.cpp @@ -0,0 +1,35 @@ +// 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: declared its registry affinity after it was first mentioned + +#include + +using namespace boost::openmethod; + +struct zoo_registry : default_registry {}; + +struct Cat; + +struct Animal { + virtual ~Animal() = default; + friend auto boost_openmethod_registry(Animal*) -> zoo_registry; +}; + +// Cat is declared but not defined, so lookup sees neither its base class nor +// the affinity it inherits: this is a `virtual_ptr`, +// and the answer is remembered. Naming the registry here would be right. +using early = virtual_ptr; + +struct Cat : Animal {}; + +// Registering Cat asks again, now that it is complete, and the two answers +// disagree. +BOOST_OPENMETHOD_CLASSES(Animal, Cat, zoo_registry); + +int main() { + return 0; +} diff --git a/test/compile_fail_adl_registry_incomplete_self.cpp b/test/compile_fail_adl_registry_incomplete_self.cpp new file mode 100644 index 00000000..765118c9 --- /dev/null +++ b/test/compile_fail_adl_registry_incomplete_self.cpp @@ -0,0 +1,36 @@ +// 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: declared its registry affinity after it was first mentioned + +#include + +#include + +using namespace boost::openmethod; + +struct zoo_registry : default_registry {}; + +struct Node { + virtual ~Node() = default; + + // The class is incomplete inside its own body, and has declared nothing + // when the member is reached: the typedef below comes too late, and so + // would a hidden friend. Move the declaration above the member. + virtual_ptr next; + + using boost_openmethod_registry = zoo_registry; +}; + +// A method over Node asks again, and the two answers disagree. +BOOST_OPENMETHOD(name, (virtual_), std::string); + +int main() { + Node node; + name(node); + + return 0; +} diff --git a/test/compile_fail_adl_registry_not_a_registry.cpp b/test/compile_fail_adl_registry_not_a_registry.cpp new file mode 100644 index 00000000..c31eeb47 --- /dev/null +++ b/test/compile_fail_adl_registry_not_a_registry.cpp @@ -0,0 +1,21 @@ +// 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: must return a registry + +#include + +using namespace boost::openmethod; + +struct Animal { + virtual ~Animal() = default; + friend auto boost_openmethod_registry(Animal*) -> int; +}; + +int main() { + (void)sizeof(virtual_ptr); + return 0; +} diff --git a/test/compile_fail_adl_registry_parameter_registry.cpp b/test/compile_fail_adl_registry_parameter_registry.cpp new file mode 100644 index 00000000..96dc6e03 --- /dev/null +++ b/test/compile_fail_adl_registry_parameter_registry.cpp @@ -0,0 +1,32 @@ +// 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 other_registry : default_registry {}; + +struct Cat { + virtual ~Cat() = default; +}; + +// A registry spelled on a parameter is not the class's affinity. Cat declares +// none, so the method lands in the default registry, and the parameter then +// contradicts it - as it did before affinities existed. The method has to name +// its registry: `BOOST_OPENMETHOD(poke, (virtual_ptr), +// void, other_registry)`. +BOOST_OPENMETHOD(poke, (virtual_ptr), void); + +int main() { + // See compile_fail_adl_registry_declared_mismatch.cpp for why the call. + Cat felix; + poke(felix); + + return 0; +} diff --git a/test/compile_fail_adl_registry_pinned_default.cpp b/test/compile_fail_adl_registry_pinned_default.cpp new file mode 100644 index 00000000..7c74e49d --- /dev/null +++ b/test/compile_fail_adl_registry_pinned_default.cpp @@ -0,0 +1,37 @@ +// 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: conflicting registry affinities + +#include + +using namespace boost::openmethod; + +struct zoo_registry : default_registry {}; + +struct Animal { + virtual ~Animal() = default; + friend auto boost_openmethod_registry(Animal*) -> zoo_registry; +}; + +// An affinity declared for the default registry is a declared affinity all the +// same: Widget does not yield to Animal the way a class that declares nothing +// would. +struct Widget { + virtual ~Widget() = default; + friend auto boost_openmethod_registry(Widget*) -> default_registry; +}; + +BOOST_OPENMETHOD( + collide, (virtual_, virtual_), void); + +int main() { + Animal animal; + Widget widget; + collide(animal, widget); + + return 0; +} diff --git a/test/compile_fail_adl_registry_ref_mismatch.cpp b/test/compile_fail_adl_registry_ref_mismatch.cpp new file mode 100644 index 00000000..98432d20 --- /dev/null +++ b/test/compile_fail_adl_registry_ref_mismatch.cpp @@ -0,0 +1,36 @@ +// 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 new file mode 100644 index 00000000..e33796b9 --- /dev/null +++ b/test/compile_fail_adl_registry_virtual_mismatch.cpp @@ -0,0 +1,34 @@ +// 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/test_adl_registry.cpp b/test/test_adl_registry.cpp index edcef792..c764c627 100644 --- a/test/test_adl_registry.cpp +++ b/test/test_adl_registry.cpp @@ -43,6 +43,66 @@ static_assert(std::is_same_v, zoo_registry>); static_assert(std::is_same_v, zoo_registry>); static_assert(std::is_same_v, default_registry>); +// A polymorphic class that happens to define `element_type` is not a smart +// pointer, and keeps its own affinity. +struct Matrix { + using element_type = double; + virtual ~Matrix() = default; + friend auto boost_openmethod_registry(Matrix*) -> zoo_registry; +}; + +static_assert(std::is_same_v, zoo_registry>); + +// cv-qualifiers on the return type are stripped. +struct Rock { + virtual ~Rock() = default; + friend auto boost_openmethod_registry(Rock*) -> const zoo_registry; +}; + +static_assert(std::is_same_v, zoo_registry>); + +struct kennel_registry : default_registry {}; + +// A member typedef declares an affinity too. It is visible from the point it +// is declared, so a class can mention `virtual_ptr` of itself in its own body, +// where it is still incomplete. +struct Node { + using boost_openmethod_registry = zoo_registry; + virtual ~Node() = default; + virtual_ptr next; +}; + +static_assert(std::is_same_v, zoo_registry>); +static_assert( + std::is_same_v>); + +// Inherited like any member, and a derived class's hides the base's. +struct Leaf : Node {}; +struct Twig : Node { + using boost_openmethod_registry = kennel_registry; +}; + +static_assert(std::is_same_v, zoo_registry>); +static_assert(std::is_same_v, kennel_registry>); + +// The typedef takes precedence over an overload - here the one Animal's +// namespace declares, and Kennel inherits. +struct Kennel : Animal { + using boost_openmethod_registry = kennel_registry; +}; + +static_assert(std::is_same_v, kennel_registry>); + +// A hidden friend that precedes the member is found too: the class is being +// defined, and lookup sees what has been declared so far. +struct Chain { + virtual ~Chain() = default; + friend auto boost_openmethod_registry(Chain*) -> zoo_registry; + virtual_ptr next; +}; + +static_assert(std::is_same_v, zoo_registry>); + // `virtual_ptr` picks it up, so `virtual_ptr` is not a `virtual_ptr` in // the default registry. static_assert(std::is_same_v, virtual_ptr>); diff --git a/test/test_adl_registry_scan.cpp b/test/test_adl_registry_scan.cpp index e8e764e8..c2630d42 100644 --- a/test/test_adl_registry_scan.cpp +++ b/test/test_adl_registry_scan.cpp @@ -75,6 +75,28 @@ static_assert( static_assert(std::is_same_v< scan, char*)>, zoo_registry>); +// A registry spelled on a `virtual_ptr` parameter is not the class's affinity: +// Widget declares none, so the method lands in the default registry - where +// the parameter then contradicts it, see +// compile_fail_adl_registry_parameter_registry.cpp. +static_assert( + std::is_same_v< + scan)>, default_registry>); + +// An affinity declared for the default registry itself is declared all the +// same: it constrains, see compile_fail_adl_registry_pinned_default.cpp. +struct Pinned { + virtual ~Pinned() = default; + friend auto boost_openmethod_registry(Pinned*) -> default_registry; +}; + +static_assert( + std::is_same_v< + detail::registry_affinity_aux::declared, default_registry>); +static_assert(std::is_same_v< + detail::registry_affinity_aux::declared, + detail::default_affinity>); + // A registry named on the declaration wins, and the parameters are not // consulted at all - the form that predates this feature. BOOST_OPENMETHOD(ping, (virtual_), std::string, other_registry);