diff --git a/CLAUDE.md b/CLAUDE.md index 690de73e..8ebc81a0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -484,6 +484,34 @@ 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 *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`). + +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: + +- `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..feb3a888 --- /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 01d158b5..6d763a72 100644 --- a/doc/modules/ROOT/pages/registries_and_policies.adoc +++ b/doc/modules/ROOT/pages/registries_and_policies.adoc @@ -50,6 +50,62 @@ 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 _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++] +---- +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. + +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 +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: 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 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)`. + 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 cad8a165..6be984af 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -129,11 +129,129 @@ 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 +//! *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. +//! +//! @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 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 + +//! 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. +//! +//! 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 registry_affinity = detail::adl_registry; template< - class Class, class Registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY, + class Class, class Registry = registry_affinity, typename = detail::sfinae> class virtual_ptr; @@ -142,8 +260,6 @@ class virtual_ptr; namespace detail { -using macro_default_registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY; - template struct extract_registry; @@ -818,7 +934,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 @@ -1711,8 +1827,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. //! @@ -1720,8 +1835,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 @@ -1946,9 +2060,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 {}; @@ -2058,14 +2169,89 @@ struct validate_method_parameter< //! selected is not specified, but it is the same across calls with the //! same arguments types. //! +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>; + +template +struct param_affinity { + using type = default_affinity; +}; + +template +struct param_affinity> { + using type = declared_affinity>; +}; + +template +struct param_affinity> { + using type = declared_affinity; +}; + +template +struct param_affinity&> { + using type = declared_affinity; +}; + +template +struct param_affinity&> { + using type = declared_affinity; +}; + +// The first affinity in the parameter list wins; every other one must agree. +template +struct agreed_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, + "virtual parameters have conflicting registry affinities"); + using type = std::conditional_t< + std::is_same_v, 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..d37d152a 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 `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 = - decltype(boost_openmethod_registry(std::declval())); +using inplace_vptr_registry = registry_affinity; 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..16dd5b91 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>; @@ -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 = BOOST_OPENMETHOD_DEFAULT_REGISTRY, - 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 49f69a84..4c9b67cb 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. @@ -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 = BOOST_OPENMETHOD_DEFAULT_REGISTRY, - 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 a6128723..30f21571 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. @@ -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 = BOOST_OPENMETHOD_DEFAULT_REGISTRY, - 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/include/boost/openmethod/macros.hpp b/include/boost/openmethod/macros.hpp index c489c398..76a35b62 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..511da69f --- /dev/null +++ b/test/compile_fail_adl_registry_declared_mismatch.cpp @@ -0,0 +1,38 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +// Expected diagnostic, as a CMake regex (see CMakeLists.txt). +// expected-error: registry mismatch + +#include + +#include + +using namespace boost::openmethod; + +struct zoo_registry : default_registry {}; +struct other_registry : default_registry {}; + +struct Animal { + virtual ~Animal() = default; + friend auto boost_openmethod_registry(Animal*) -> zoo_registry; +}; + +// `virtual_ptr` carries `zoo_registry`, because that is what `Animal` +// says. Declaring the method in another registry contradicts the parameter - +// the check that already guarded an explicitly spelled `virtual_ptr`. +BOOST_OPENMETHOD(poke, (virtual_ptr), std::string, other_registry); + +int main() { + // The check is a fold in `method`'s class body, so it needs the class to be + // instantiated. Declaring the method is not enough: gcc and clang + // instantiate it anyway through the registrar, but MSVC does not, and the + // file then compiles. Calling it forces the point, on every compiler - like + // compile_fail_virtual_ptr_different_registries.cpp does. + Animal animal; + poke(animal); + + return 0; +} diff --git a/test/compile_fail_adl_registry_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..edcef792 --- /dev/null +++ b/test/test_adl_registry.cpp @@ -0,0 +1,91 @@ +// 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..a7971f57 --- /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..5212b33d --- /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..1c6b4d92 --- /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< + bom::policies::vptr, bom::policies::type_hash> {}; + +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 `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); + +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..e8e764e8 --- /dev/null +++ b/test/test_adl_registry_scan.cpp @@ -0,0 +1,91 @@ +// 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. 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 + +#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 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_)>, + 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..2137433f --- /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>, zoo_registry>); +static_assert(std::is_same_v< + registry_affinity&>, 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..de29dfe2 --- /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..66ffd276 --- /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"); +}