From e7f1970630d44a046f2e45a2ea2b8d2b5bde0155 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Tue, 15 Sep 2026 08:49:36 -0400 Subject: [PATCH] registry affinity: correct the reference, and stop expanding va_args thrice Follow-ups from the review of #96, all mechanical. The reference still described the pre-affinity default in five places: the `BOOST_OPENMETHOD_DEFAULT_REGISTRY` page ("the default value for the Registry template parameter of method, use_classes, virtual_ptr, and all the constructs that take a registry" - only `use_classes` is still true), the `virtual_ptr` class, the one-argument `final_virtual_ptr`, and the `@note` on each of `BOOST_OPENMETHOD` and `BOOST_OPENMETHOD_CLASSES`. Each now says what the construct actually defaults to, and the `BOOST_OPENMETHOD_CLASSES` note says plainly that it does *not* consult affinities - the trap the page warns about. `registries_and_policies.adoc` gets the same correction in its opening paragraph, and `class method`'s own description still carried a paragraph contradicting its `@tparam Registry`. That description was also missing from the reference entirely: MrDocs does not attach a `//!` comment to a declaration when a namespace definition comes between them, and the `namespace detail { ... }` holding the affinity scan had been inserted there. The block moves above the comment, which now touches `class method` as it must. Rendered `method` page keeps its call semantics and overrider-selection steps. `BOOST_OPENMETHOD_TYPE` expanded `va_args<__VA_ARGS__>` three times and the parameter list twice, to compute a registry and hand it back to `method`: 30-36% more preprocessor output per declaration, measured with `g++ -E -P`. A `method_type` alias on the two `va_args` specializations names the method directly, so the macro spells `va_args` twice and the parameter list once, and the affinity scan still never runs for a declaration that names a registry. `va_args::registry` had no consumer left in the tree and goes. Also: three explanatory comments inside the tagged regions of `adl_registry.cpp` moved to the page that includes them, per the rule in CLAUDE.md, and two tests stop naming `BOOST_OPENMETHOD_DEFAULT_REGISTRY` in a comment - `test/CMakeLists.txt` scans for that token to decide which tests must not get the shared PCH, and a mention in prose was enough to withhold it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RQG6CbE4o2agseE7bDVzHS --- doc/modules/ROOT/examples/adl_registry.cpp | 3 - .../ROOT/pages/registries_and_policies.adoc | 14 +- include/boost/openmethod/core.hpp | 171 +++++++++--------- include/boost/openmethod/macros.hpp | 49 ++--- test/test_adl_registry_inplace.cpp | 2 +- test/test_adl_registry_static_rtti.cpp | 2 +- 6 files changed, 122 insertions(+), 119 deletions(-) diff --git a/doc/modules/ROOT/examples/adl_registry.cpp b/doc/modules/ROOT/examples/adl_registry.cpp index ff83af45..4584adc5 100644 --- a/doc/modules/ROOT/examples/adl_registry.cpp +++ b/doc/modules/ROOT/examples/adl_registry.cpp @@ -27,7 +27,6 @@ class Animal { virtual ~Animal() = default; private: - // Animal - and every class derived from it - belongs to zoo_registry friend auto boost_openmethod_registry(Animal*) -> zoo_registry; }; @@ -40,7 +39,6 @@ class Cat : public Animal {}; // 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) { @@ -53,7 +51,6 @@ BOOST_OPENMETHOD_OVERRIDE(speak, (const zoo::Cat&), std::string) { // end::methods[] // tag::virtual_ptr[] -// ...and so does virtual_ptr static_assert( std::is_same_v, virtual_ptr>); // end::virtual_ptr[] diff --git a/doc/modules/ROOT/pages/registries_and_policies.adoc b/doc/modules/ROOT/pages/registries_and_policies.adoc index 5795cdaa..e6e1b858 100644 --- a/doc/modules/ROOT/pages/registries_and_policies.adoc +++ b/doc/modules/ROOT/pages/registries_and_policies.adoc @@ -14,9 +14,10 @@ preprocessor symbol xref:reference:BOOST_OPENMETHOD_DEFAULT_REGISTRY.adoc[BOOST_OPENMETHOD_DEFAULT_REGISTRY] _before_ including `` (or any header that includes it, like ``). The value of the symbol is used as a default -template parameter for `use_classes`, `method`, `virtual_ptr`, and others. Once -it has been included, changing `BOOST_OPENMETHOD_DEFAULT_REGISTRY` has no -effect. +template parameter for `use_classes`, and for `method` and `virtual_ptr` where +the classes involved declare no affinity of their own - see <> below. Once it has been included, changing +`BOOST_OPENMETHOD_DEFAULT_REGISTRY` has no effect. For a registry the library provides, that is the whole recipe: @@ -63,9 +64,10 @@ it needs no definition: 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: +The class then _declares_ an affinity for that registry - and so does every +class derived from it - and everything that mentions the class finds it. A +method declared without a registry argument takes the affinity of its virtual +parameters: [source,c++] ---- diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index b7d5920d..cd4f4856 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -29,10 +29,13 @@ //! //! The name of the default registry. //! -//! `BOOST_OPENMETHOD_DEFAULT_REGISTRY` is the default value for the `Registry` -//! template parameter of @ref boost::openmethod::method, -//! @ref boost::openmethod::use_classes, @ref boost::openmethod::virtual_ptr, -//! and all the constructs that take a registry as a template argument. +//! `BOOST_OPENMETHOD_DEFAULT_REGISTRY` is the registry that a construct taking +//! one as a template argument uses when neither the construct nor the class it +//! is about names another: directly, as @ref boost::openmethod::use_classes +//! does, or as the registry a class has an affinity for when it declares none +//! - which is what @ref boost::openmethod::method and +//! @ref boost::openmethod::virtual_ptr default to. See +//! @ref boost::openmethod::registry_affinity. //! //! `BOOST_OPENMETHOD_DEFAULT_REGISTRY` can be defined by a program to change //! the default registry globally, *before* including @@ -1045,8 +1048,9 @@ inline auto final_virtual_ptr(Arg&& obj) { //! Create a `virtual_ptr` for an object of a known exact class. //! -//! This is an overload of `final_virtual_ptr` that uses the default -//! registry as the `Registry` template parameter. +//! This is an overload of `final_virtual_ptr` that uses the registry the +//! object's class has an affinity for - see @ref registry_affinity - as the +//! `Registry` template parameter. //! //! @par Example //! @@ -1075,9 +1079,9 @@ inline auto final_virtual_ptr(Arg&& obj) { //! "plain" `virtual_ptr` can be constructed from a smart `virtual_ptr`, but not //! the other way around. //! -//! The default value for `Registry` can be customized by defining the -//! @ref BOOST_OPENMETHOD_DEFAULT_REGISTRY -//! preprocessor symbol. +//! `Registry` defaults to the registry `Class` has an affinity for - see +//! @ref registry_affinity - which is @ref BOOST_OPENMETHOD_DEFAULT_REGISTRY +//! for a class that declares none. //! //! @par Requirements //! @@ -2259,6 +2263,76 @@ struct validate_method_parameter< }; } // namespace detail +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. 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; +}; + +template +struct param_affinity> { + using type = typename registry_affinity_aux::declared; +}; + +template +struct param_affinity> { + using type = typename registry_affinity_aux::declared; +}; + +template +struct param_affinity&> { + using type = typename registry_affinity_aux::declared; +}; + +template +struct param_affinity&> { + using type = typename registry_affinity_aux::declared; +}; + +// 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 + //! Implement a method //! //! Methods are created by specializing the `method` class template with an @@ -2277,11 +2351,10 @@ struct validate_method_parameter< //! acquire a v-table pointer for an object, how to report errors, whether to //! perform sanity checks, etc. //! -//! The default value for `Registry` is @ref default_registry, but it can be -//! overridden by defining the preprocessor symbol -//! @ref BOOST_OPENMETHOD_DEFAULT_REGISTRY, *before* including -//! `` (or any header that includes it, like -//! ``). Setting the symbol afterwards has no effect. +//! `Registry` defaults to the registry the virtual parameters of `Fn` have an +//! affinity for - see @ref registry_affinity - and to +//! @ref BOOST_OPENMETHOD_DEFAULT_REGISTRY when none of them declares one. +//! Parameters that declare different registries are an error. //! //! Specializations of `method` have a single instance: the static member `fn`, //! which has an `operator()` that forwards to the appropriate overrider. It is @@ -2335,76 +2408,6 @@ 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. 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; -}; - -template -struct param_affinity> { - using type = typename registry_affinity_aux::declared; -}; - -template -struct param_affinity> { - using type = typename registry_affinity_aux::declared; -}; - -template -struct param_affinity&> { - using type = typename registry_affinity_aux::declared; -}; - -template -struct param_affinity&> { - using type = typename registry_affinity_aux::declared; -}; - -// 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. Defaults to diff --git a/include/boost/openmethod/macros.hpp b/include/boost/openmethod/macros.hpp index 4181550c..f87df558 100644 --- a/include/boost/openmethod/macros.hpp +++ b/include/boost/openmethod/macros.hpp @@ -25,26 +25,25 @@ 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. +// `method_type` names the method rather than yielding a registry for +// BOOST_OPENMETHOD_TYPE to plug in, so that the macro spells the parameter +// list once. It is an alias template, so the specialization that omits the +// registry leaves `method`'s own default to scan the parameters for an +// affinity, and the one that names a registry never triggers that scan. template struct va_args { using return_type = ReturnType; - using registry = macro_default_registry; - template - using registry_for = method_registry; + template + using method_type = method; }; template struct va_args { using return_type = ReturnType; - using registry = Registry; - template - using registry_for = Registry; + template + using method_type = method; }; template @@ -125,13 +124,10 @@ inline constexpr bool method_not_found = false; //! //! @see [Core API](xref:ROOT:core_api.adoc) #define BOOST_OPENMETHOD_TYPE(ID, PARAMETERS, ...) \ - ::boost::openmethod::method< \ + ::boost::openmethod::detail::va_args<__VA_ARGS__>::method_type< \ BOOST_OPENMETHOD_ID(ID), \ ::boost::openmethod::detail::va_args<__VA_ARGS__>::return_type \ - PARAMETERS, \ - ::boost::openmethod::detail::va_args<__VA_ARGS__>::registry_for< \ - ::boost::openmethod::detail::va_args<__VA_ARGS__>::return_type \ - PARAMETERS>> + PARAMETERS> //! Declare a method. //! @@ -187,11 +183,13 @@ inline constexpr bool method_not_found = false; //! //! @note `ID` must be an *identifier*. Qualified names are not allowed. //! -//! @note The default registry is the value of -//! @ref BOOST_OPENMETHOD_DEFAULT_REGISTRY at the point -//! `` is included, directly or through a header -//! like ``. Changing the value of this symbol has no -//! effect after that point. +//! @note A declaration that does not name a registry takes the one its virtual +//! parameters have an affinity for - see +//! @ref boost::openmethod::registry_affinity - and +//! @ref BOOST_OPENMETHOD_DEFAULT_REGISTRY when none of them declares one. That +//! symbol is read at the point `` is included, +//! directly or through a header like ``; changing its +//! value has no effect after that point. //! //! @par Example //! @@ -550,10 +548,13 @@ inline constexpr bool method_not_found = false; //! This macro is a wrapper around @ref boost::openmethod::use_classes; see its //! documentation for more details. //! -//! @note The default registry is the value of -//! @ref BOOST_OPENMETHOD_DEFAULT_REGISTRY when `` -//! is included, directly or through a header like ``. -//! Subsequently changing it has no retroactive effect. +//! @note Unlike a method declaration, this macro does not consult the classes' +//! registry affinities: without a registry in the list it registers into +//! @ref BOOST_OPENMETHOD_DEFAULT_REGISTRY, whatever the classes declare. List +//! the registry last when they declare one. The symbol is read when +//! `` is included, directly or through a header +//! like ``; subsequently changing it has no retroactive +//! effect. //! //! @par Examples //! diff --git a/test/test_adl_registry_inplace.cpp b/test/test_adl_registry_inplace.cpp index 1c6b4d92..78b9f87d 100644 --- a/test/test_adl_registry_inplace.cpp +++ b/test/test_adl_registry_inplace.cpp @@ -5,7 +5,7 @@ // `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. +// registry argument nor an override of the default-registry macro. #include diff --git a/test/test_adl_registry_static_rtti.cpp b/test/test_adl_registry_static_rtti.cpp index de29dfe2..16ce07d5 100644 --- a/test/test_adl_registry_static_rtti.cpp +++ b/test/test_adl_registry_static_rtti.cpp @@ -4,7 +4,7 @@ // 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 +// affinity instead of by the default-registry macro. 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.