From 113759036bc67d51c1955b3bc515cc6039a74e38 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Tue, 13 Oct 2026 17:36:23 -0400 Subject: [PATCH] interop: check the registry spelled on an `any` parameter (#123) The validate_method_parameter specializations in interop/std_any.hpp, interop/boost_any.hpp and interop/boost_type_erasure.hpp accepted every parameter of their shape unconditionally, so they never compared the registry spelled on one against the method's. Every other virtual parameter shape rejects a mismatch; this one took it silently, while still deciding the registry of a method that named none - so overriders registered against another registry landed in a different dispatch table, and the call reported no_overrider after initialize(). Those specializations were there only because the generic rule did not count a virtual_traits::vptr as a way to reach a v-table. It knew two of the three routes method::vptr tries, the ADL hook and a class the rtti policy deems polymorphic, and a type-erased value has neither. detail::has_vptr_source now names all three, in the order they are tried, so the generic rule covers the interop shapes and applies its registry check and affinity recheck to them. The eleven blanket specializations are gone; boost_type_erasure.hpp keeps the one that is a rejection rather than a blanket accept, an owning any passed by value. The spelling of the new route has to be the one method::vptr uses, `has_vptr, decltype(peek(declval()))>`: the shorter `const virtual_type&` holds for ten of the eleven shapes and fails for `any`. Stashing include/ brings the issue's reproducer back: it compiles there, and is now rejected with "registry mismatch: the parameter belongs to another registry", the message an ordinary carrier gets. The static_assert naming the routes gains the third; it keeps the substring the compile-fail test matches. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 16 ++++++--- include/boost/openmethod/core.hpp | 24 +++++++++++--- .../boost/openmethod/interop/boost_any.hpp | 13 -------- .../openmethod/interop/boost_type_erasure.hpp | 25 -------------- include/boost/openmethod/interop/std_any.hpp | 13 -------- ...compile_fail_std_any_registry_mismatch.cpp | 33 +++++++++++++++++++ test/test_dispatch_std_any.cpp | 16 +++++++++ 7 files changed, 79 insertions(+), 61 deletions(-) create mode 100644 test/compile_fail_std_any_registry_mismatch.cpp diff --git a/CLAUDE.md b/CLAUDE.md index acefc627..548a920c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -632,7 +632,7 @@ interop shapes differ from one another. What each does, with the method on anoth | parameter spelling `R` | decides an unannotated method's registry | checked against a method naming another | |---|---|---| | `virtual_` (ordinary carrier) | yes | yes - `core.hpp`, "the parameter belongs to another registry" | -| `virtual_` (interop) | yes | **no - nothing checks it** (#123) | +| `virtual_` (interop) | yes | yes - the same check, since #123 | | `virtual_any&` | no, abstains | yes - `virtual_any.hpp`, "registry mismatch" | - `interop/std_any.hpp`, `interop/boost_any.hpp`, `interop/boost_type_erasure.hpp` and @@ -640,10 +640,16 @@ interop shapes differ from one another. What each does, with the method on anoth keeps whatever affinity it declares itself. A parameter may still *spell* a registry - a different thing from declaring an affinity, and the reason #116 gave the `validate_method_parameter` specializations the registry parameter `virtual_` gained in #113. - A specialization there must never go back to the bare `virtual_` spelling. Those - specializations accept any `ParamRegistry` without ever comparing it, which is the middle row - above and a bug rather than a decision (#123); `virtual_any`'s are a different shape - (`virtual_any&`) and do compare. None of this is covered in `doc/`. + **Those three headers no longer specialize `validate_method_parameter` at all** (#123). They + used to accept every parameter of their shape unconditionally, which also waved the registry + check through - the middle row above, a bug rather than a decision. They were there only + because the generic rule did not recognize a `virtual_traits::vptr` as a way to reach a + v-table; `detail::has_vptr_source` now names all three routes (the ADL hook, the parameter's + `virtual_traits`, and `is_polymorphic`), so the generic rule covers the interop shapes and + applies its registry check and affinity recheck to them. `interop/boost_type_erasure.hpp` + keeps one specialization, and it is a *rejection*: an owning `any` passed by value. + `virtual_any`'s are a different shape (`virtual_any&`) and do compare. None of + this is covered in `doc/`. - The C++26 `register_classes` still defaults to the macro - its groups may name a namespace, whose classes are only known during the scan that the choice of registry feeds. diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index f3a7c474..27f4552e 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -2316,6 +2316,22 @@ template struct parameter_traits&, Registry> : virtual_traits&, Registry> {}; +// Whether a virtual parameter can yield a v-table pointer at all, by any of +// the three routes `method::vptr` tries, in that order: the ADL hook on the +// class, the parameter's own `virtual_traits` - which is how the interop +// headers dispatch on a type-erased value - and the `rtti` policy being able +// to read the class of an object, which `is_polymorphic` reports. +// +// A trait of its own because a condition spanning the three does not fit the +// indentation, and because each route is worth naming. +template +constexpr bool has_vptr_source = + has_vptr_fn, Registry> || + has_vptr< + virtual_traits, + decltype(virtual_traits::peek(std::declval()))> || + Registry::rtti::template is_polymorphic>; + template struct validate_method_parameter : std::true_type {}; @@ -2329,13 +2345,11 @@ template struct validate_method_parameter< virtual_, Registry, std::void_t::virtual_type>> : - std::bool_constant< - has_vptr_fn, Registry> || - Registry::rtti::template is_polymorphic>> { + std::bool_constant> { static_assert( validate_method_parameter::value, - "virtual_<> parameter is not a polymorphic class and no " - "boost_openmethod_vptr is applicable"); + "virtual_<> parameter is not a polymorphic class, and neither a " + "boost_openmethod_vptr nor a virtual_traits 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. diff --git a/include/boost/openmethod/interop/boost_any.hpp b/include/boost/openmethod/interop/boost_any.hpp index 8ba304fc..ee1e6f2e 100644 --- a/include/boost/openmethod/interop/boost_any.hpp +++ b/include/boost/openmethod/interop/boost_any.hpp @@ -20,19 +20,6 @@ namespace boost::openmethod { namespace detail { -template -struct validate_method_parameter< - virtual_, Registry, void> : - std::true_type {}; - -template -struct validate_method_parameter< - virtual_, Registry, void> : std::true_type {}; - -template -struct validate_method_parameter< - virtual_, Registry, void> : std::true_type {}; - // `boost::any::type()` yields a `std::type_info`, which is a valid `type_id` // only for an rtti policy that identifies classes by `&typeid(T)`. Under any // other policy the lookup key is meaningless, and `type_id` being diff --git a/include/boost/openmethod/interop/boost_type_erasure.hpp b/include/boost/openmethod/interop/boost_type_erasure.hpp index 0cfba56d..452b13fa 100644 --- a/include/boost/openmethod/interop/boost_type_erasure.hpp +++ b/include/boost/openmethod/interop/boost_type_erasure.hpp @@ -94,31 +94,6 @@ constexpr bool te_pass_through = // placeholder of the parameter, so methods and overriders agree on a // single registered root per Concept. -template -struct validate_method_parameter< - virtual_&, ParamRegistry>, Registry, - void> : std::true_type {}; - -template -struct validate_method_parameter< - virtual_&, ParamRegistry>, Registry, void> : - std::true_type {}; - -template -struct validate_method_parameter< - virtual_&&, ParamRegistry>, Registry, void> : - std::true_type {}; - -template -struct validate_method_parameter< - virtual_, ParamRegistry>, Registry, void> : - std::true_type {}; - -template -struct validate_method_parameter< - virtual_, ParamRegistry>, Registry, - void> : std::true_type {}; - template struct validate_method_parameter< virtual_< diff --git a/include/boost/openmethod/interop/std_any.hpp b/include/boost/openmethod/interop/std_any.hpp index f80fe1d2..d8ff4a89 100644 --- a/include/boost/openmethod/interop/std_any.hpp +++ b/include/boost/openmethod/interop/std_any.hpp @@ -20,19 +20,6 @@ namespace boost::openmethod { namespace detail { -template -struct validate_method_parameter< - virtual_, Registry, void> : - std::true_type {}; - -template -struct validate_method_parameter< - virtual_, Registry, void> : std::true_type {}; - -template -struct validate_method_parameter< - virtual_, Registry, void> : std::true_type {}; - // `std::any::type()` yields a `std::type_info`, which is a valid `type_id` // only for an rtti policy that identifies classes by `&typeid(T)`. Under any // other policy the lookup key is meaningless, and `type_id` being diff --git a/test/compile_fail_std_any_registry_mismatch.cpp b/test/compile_fail_std_any_registry_mismatch.cpp new file mode 100644 index 00000000..bbf58697 --- /dev/null +++ b/test/compile_fail_std_any_registry_mismatch.cpp @@ -0,0 +1,33 @@ +// 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 + +#include +#include + +using namespace boost::openmethod; + +struct zoo_registry : default_registry {}; + +// An interop parameter carries a registry exactly as an ordinary one does - +// see compile_fail_virtual_registry_mismatch.cpp - and must agree with its +// method. It used to be accepted silently, while still deciding the registry +// of a method that named none (#123). +BOOST_OPENMETHOD( + speak, (virtual_), std::string, + default_registry); + +int main() { + // See compile_fail_adl_registry_declared_mismatch.cpp for why the call. + std::any animal; + speak(animal); + + return 0; +} diff --git a/test/test_dispatch_std_any.cpp b/test/test_dispatch_std_any.cpp index 0b6545af..32463be5 100644 --- a/test/test_dispatch_std_any.cpp +++ b/test/test_dispatch_std_any.cpp @@ -41,6 +41,22 @@ static_assert(detail::validate_method_parameter< static_assert(detail::validate_method_parameter< virtual_, default_registry, void>::value); +// The parameter's own virtual_traits is what yields its v-table pointer: the +// third way a virtual parameter can have one, next to the ADL hook and a +// class the rtti policy deems polymorphic. Before #123 the interop headers +// said so by accepting every parameter of this shape unconditionally, which +// also waved the registry check through. +static_assert(detail::has_vptr_source); +static_assert(detail::has_vptr_source); +static_assert(detail::has_vptr_source); + +// An interop parameter is an ordinary carrier: it decides the registry of a +// method that names none, and compile_fail_std_any_registry_mismatch.cpp +// covers the other half, a method that names a different one. +static_assert(std::is_same_v< + detail::method_registry)>, + default_registry>); + MAKE_CLASSES(); BOOST_OPENMETHOD(name, (virtual_), std::string);