From f9aacc6e1a8a19c2b1888539d031d5e6eaae760a Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 25 Jan 2026 21:15:34 -0500 Subject: [PATCH 01/64] BOOST_OPENMETHOD_DETAIL_HAS_STATIC_FN --- include/boost/openmethod/preamble.hpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/include/boost/openmethod/preamble.hpp b/include/boost/openmethod/preamble.hpp index 39b82c6f..4f9c3e03 100644 --- a/include/boost/openmethod/preamble.hpp +++ b/include/boost/openmethod/preamble.hpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -869,6 +870,17 @@ struct initialize_aux; } // namespace detail +#define BOOST_OPENMETHOD_DETAIL_HAS_STATIC_FN(FN) \ + template \ + struct BOOST_PP_CAT(has_, BOOST_PP_CAT(FN, _aux)) : std::false_type {}; \ + template \ + struct BOOST_PP_CAT(has_, BOOST_PP_CAT(FN, _aux))< \ + std::void_t()...))>, T, Args...> \ + : std::true_type {}; \ + template \ + constexpr bool BOOST_PP_CAT(has_, FN) = \ + BOOST_PP_CAT(has_, BOOST_PP_CAT(FN, _aux))::value + //! Methods, classes and policies. //! //! Methods exist in the context of a registry. Any class used as a method or From 1eb22d8877e6ca498beb4996b8c413f048d3ecbc Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 28 Feb 2026 17:09:34 -0500 Subject: [PATCH 02/64] inter-operate with 'any' --- include/boost/openmethod/core.hpp | 9 +- include/boost/openmethod/interop/std_any.hpp | 196 ++++++++++++++++++ .../boost/openmethod/policies/vptr_map.hpp | 15 +- .../boost/openmethod/policies/vptr_vector.hpp | 24 ++- test/test_dispatch_std_any.cpp | 118 +++++++++++ 5 files changed, 356 insertions(+), 6 deletions(-) create mode 100644 include/boost/openmethod/interop/std_any.hpp create mode 100644 test/test_dispatch_std_any.cpp diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index 7c2d3837..6e8cb9a9 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -525,12 +525,19 @@ constexpr bool has_vptr_fn = std::is_same_v< std::declval(), std::declval())), vptr_type>; +BOOST_OPENMETHOD_DETAIL_HAS_STATIC_FN(dynamic_vptr); + template decltype(auto) acquire_vptr(const ArgType& arg) { Registry::require_initialized(); - if constexpr (detail::has_vptr_fn) { + if constexpr (has_vptr_fn) { return boost_openmethod_vptr(arg, static_cast(nullptr)); + } else if constexpr (has_dynamic_vptr< + virtual_traits, + type_id>) { + return virtual_traits::dynamic_vptr( + arg); } else { return Registry::template policy::dynamic_vptr(arg); } diff --git a/include/boost/openmethod/interop/std_any.hpp b/include/boost/openmethod/interop/std_any.hpp new file mode 100644 index 00000000..89af1e59 --- /dev/null +++ b/include/boost/openmethod/interop/std_any.hpp @@ -0,0 +1,196 @@ +// Copyright (c) 2018-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) + +#ifndef BOOST_OPENMETHOD_INTEROP_STD_ANY_HPP +#define BOOST_OPENMETHOD_INTEROP_STD_ANY_HPP + +#include +#include + +namespace boost::openmethod { + +namespace detail { +template +struct validate_method_parameter, Registry, void> + : std::true_type {}; + +template +struct validate_method_parameter, Registry, void> + : std::true_type {}; + +template +struct validate_method_parameter, Registry, void> + : std::true_type {}; + +template +struct validate_method_parameter, Registry, void> + : std::true_type {}; + +} // namespace detail + +//! Specialize virtual_traits for std::any by value. +//! +//! Dispatch is based on the runtime type of the value stored in the `any`, +//! obtained via `std::any::type()`. Requires the registry to use a @ref +//! rtti policy that provides `dynamic_type` (e.g. @ref std_rtti). +//! +//! @tparam Registry A @ref registry. +template +struct virtual_traits { + //! The type used for dispatch. + using virtual_type = std::any; + + //! Returns a const reference to the `any` argument. + //! @param arg A reference to a `std::any`. + //! @return A const reference to `arg`. + static auto peek(const std::any& arg) -> const std::any& { + return arg; + } + + //! Returns a *reference* to a v-table pointer for an object. + //! + //! Acquires the dynamic @ref type_id of `arg`, using the registry's + //! @ref rtti policy. + //! + //! If the registry has a @ref type_hash policy, uses it to convert the + //! type id to an index; otherwise, uses the type_id as the index. + //! + //! If the registry contains the @ref runtime_checks policy, verifies + //! that the index falls within the limits of the vector. If it does + //! not, and if the registry contains a @ref error_handler policy, calls + //! its @ref error function with a @ref missing_class value, then + //! terminates the program with @ref abort. + //! + //! @param arg A reference to a const `any`. + //! @return A reference to a the v-table pointer for `Class`. + static auto dynamic_vptr(const std::any& arg) -> const vptr_type& { + return Registry::rtti::type_vptr(arg.type()); + }; + + //! Cast to a type. + //! + //! Extracts the stored value using `std::any_cast`. + //! + //! @tparam U The target type (e.g. `Dog&`, `const Dog&`, `Dog`). + //! @param arg An rvalue reference to the `std::any` method argument. + //! @return The value stored in `arg`, cast to `U`. + template + static auto cast(std::any&& arg) -> decltype(auto) { + return std::any_cast(arg); + } +}; + +//! Specialize virtual_traits for `std::any&` (mutable reference). +//! +//! @tparam Registry A @ref registry. +template +struct virtual_traits { + //! The type used for dispatch. + using virtual_type = std::any; + + //! Returns a const reference to the `any` argument. + //! @param arg A reference to a `std::any`. + //! @return A const reference to `arg`. + static auto peek(const std::any& arg) -> const std::any& { + return arg; + } + + //! Returns a *reference* to a v-table pointer for an object. + //! + //! Acquires the dynamic @ref type_id of `arg`, using the registry's + //! @ref rtti policy. + //! + //! If the registry has a @ref type_hash policy, uses it to convert the + //! type id to an index; otherwise, uses the type_id as the index. + //! + //! If the registry contains the @ref runtime_checks policy, verifies + //! that the index falls within the limits of the vector. If it does + //! not, and if the registry contains a @ref error_handler policy, calls + //! its @ref error function with a @ref missing_class value, then + //! terminates the program with @ref abort. + //! + //! @param arg A reference to a const `any`. + //! @return A reference to a the v-table pointer for `Class`. + static auto dynamic_vptr(const std::any& arg) -> const vptr_type& { + return Registry::vptr::type_vptr(&arg.type()); + }; + + //! Cast to a type. + //! + //! Extracts the stored value using `std::any_cast`. Supports mutable + //! references (e.g. `Dog&`) because the `any` argument is non-const. + //! + //! @tparam U The target type (e.g. `Dog&`, `const Dog&`, `Dog`). + //! @param arg A mutable reference to the `std::any` method argument. + //! @return The value stored in `arg`, cast to `U`. + template + static auto cast(const std::any& arg) -> decltype(auto) { + return std::any_cast(arg); + } +}; + +//! Specialize virtual_traits for std::any by value. +//! +//! Dispatch is based on the runtime type of the value stored in the `any`, +//! obtained via `std::any::type()`. Requires the registry to use a @ref +//! rtti policy that provides `dynamic_type` (e.g. @ref std_rtti). +//! +//! @tparam Registry A @ref registry. +template +struct virtual_traits { + //! The type used for dispatch. + using virtual_type = std::any; + + //! Returns a const reference to the `any` argument. + //! @param arg A reference to a `std::any`. + //! @return A const reference to `arg`. + static auto peek(const std::any& arg) -> const std::any& { + return arg; + } + + //! Returns a *reference* to a v-table pointer for an object. + //! + //! Acquires the dynamic @ref type_id of `arg`, using the registry's + //! @ref rtti policy. + //! + //! If the registry has a @ref type_hash policy, uses it to convert the + //! type id to an index; otherwise, uses the type_id as the index. + //! + //! If the registry contains the @ref runtime_checks policy, verifies + //! that the index falls within the limits of the vector. If it does + //! not, and if the registry contains a @ref error_handler policy, calls + //! its @ref error function with a @ref missing_class value, then + //! terminates the program with @ref abort. + //! + //! @param arg A reference to a const `any`. + //! @return A reference to a the v-table pointer for `Class`. + static auto dynamic_vptr(const std::any& arg) -> const vptr_type& { + return Registry::rtti::type_vptr(arg.type()); + }; + + //! Cast to a type. + //! + //! Extracts the stored value using `std::any_cast`. + //! + //! @tparam U The target type (e.g. `Dog&`, `const Dog&`, `Dog`). + //! @param arg An rvalue reference to the `std::any` method argument. + //! @return The value stored in `arg`, cast to `U`. + template + static auto cast(std::any&& arg) -> decltype(auto) { + return std::any_cast(arg); + } +}; + +template +struct use_any_types : detail::use_class_aux< + typename detail::extract_registry::registry, + mp11::mp_list>, + detail::use_class_aux< + typename detail::extract_registry::registry, + mp11::mp_list>... {}; + +} // namespace boost::openmethod + +#endif diff --git a/include/boost/openmethod/policies/vptr_map.hpp b/include/boost/openmethod/policies/vptr_map.hpp index c26e5de2..33a12b5d 100644 --- a/include/boost/openmethod/policies/vptr_map.hpp +++ b/include/boost/openmethod/policies/vptr_map.hpp @@ -79,7 +79,20 @@ class vptr_map : public vptr { //! @return A reference to a the v-table pointer for `Class`. template static auto dynamic_vptr(const Class& arg) -> const vptr_type& { - auto type = Registry::rtti::dynamic_type(arg); + return type_vptr(Registry::rtti::dynamic_type(arg)); + } + + //! Returns a *reference* to a v-table pointer for a type. + //! + //! If the registry contains the @ref runtime_checks policy, checks that + //! the map contains the type id. If it does not, and if the registry + //! contains a @ref error_handler policy, calls its + //! @ref error function with a @ref missing_class value, then + //! terminates the program with @ref abort. + //! + //! @param type A `type_id`. + //! @return A reference to a the v-table pointer for `type`. + static auto type_vptr(type_id type) -> const vptr_type& { auto iter = vptrs.find(type); if constexpr (Registry::has_runtime_checks) { diff --git a/include/boost/openmethod/policies/vptr_vector.hpp b/include/boost/openmethod/policies/vptr_vector.hpp index 1b1a2768..5020ba07 100644 --- a/include/boost/openmethod/policies/vptr_vector.hpp +++ b/include/boost/openmethod/policies/vptr_vector.hpp @@ -133,12 +133,28 @@ struct vptr_vector : vptr { //! @return A reference to a the v-table pointer for `Class`. template static auto dynamic_vptr(const Class& arg) -> const vptr_type& { - auto dynamic_type = Registry::rtti::dynamic_type(arg); + return type_vptr(Registry::rtti::dynamic_type(arg)); + }; + + //! Returns a *reference* to a v-table pointer for a type. + //! + //! If the registry has a @ref type_hash policy, uses it to convert the + //! type id to an index; otherwise, uses the type_id as the index. + //! + //! If the registry contains the @ref runtime_checks policy, verifies + //! that the index falls within the limits of the vector. If it does + //! not, and if the registry contains a @ref error_handler policy, calls + //! its @ref error function with a @ref missing_class value, then + //! terminates the program with @ref abort. + //! + //! @param type A `type_id`. + //! @return A reference to a the v-table pointer for `type`. + static auto type_vptr(type_id type) -> const vptr_type& { std::size_t index; if constexpr (has_type_hash) { - index = type_hash::hash(dynamic_type); + index = type_hash::hash(type); } else { - index = std::size_t(dynamic_type); + index = std::size_t(type); if constexpr (Registry::has_runtime_checks) { std::size_t max_index = 0; @@ -153,7 +169,7 @@ struct vptr_vector : vptr { if (index >= max_index) { if constexpr (Registry::has_error_handler) { missing_class error; - error.type = dynamic_type; + error.type = type; Registry::error_handler::error(error); } diff --git a/test/test_dispatch_std_any.cpp b/test/test_dispatch_std_any.cpp new file mode 100644 index 00000000..258b31b8 --- /dev/null +++ b/test/test_dispatch_std_any.cpp @@ -0,0 +1,118 @@ +// Copyright (c) 2018-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +#include + +#include +#include +#include + +#define BOOST_TEST_MODULE openmethod +#include + +using namespace boost::openmethod; + +#define MAKE_CLASSES() \ + struct Dog { \ + std::string name; \ + }; \ + \ + use_any_types BOOST_OPENMETHOD_GENSYM; + +#if 0 + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as std::any by value + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (Dog dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (Cat cat), std::string) { + return cat.name + " the cat"; +} + +BOOST_AUTO_TEST_CASE(std_any_by_value) { + initialize(); + + BOOST_TEST(name(std::any(Dog{"Spot"})) == "Spot the dog"); + BOOST_TEST(name(std::any(Cat{"Felix"})) == "Felix the cat"); +} +} // namespace BOOST_OPENMETHOD_GENSYM +#endif +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as const std::any& (const ref) + +static_assert(detail::has_dynamic_vptr< + virtual_traits, type_id>); + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { + return name; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const int& value), std::string) { + std::ostringstream os; + os << value << " the integer"; + return os.str(); +} + +BOOST_AUTO_TEST_CASE(std_any_by_const_ref) { + initialize(trace()); + + const std::any spot(Dog{"Spot"}); + const std::any felix(std::string{"Felix the cat"}); + const std::any answer(42); + + BOOST_TEST(name(spot) == "Spot the dog"); + BOOST_TEST(name(felix) == "Felix the cat"); + BOOST_TEST(name(answer) == "42 the integer"); +} +} // namespace BOOST_OPENMETHOD_GENSYM +#if 0 +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as std::any&& (rvalue ref, move semantics) + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (Dog dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (Cat cat), std::string) { + return cat.name + " the cat"; +} + +BOOST_AUTO_TEST_CASE(std_any_by_rvalue_ref) { + initialize(); + + std::any spot(Dog{"Spot"}); + std::any felix(Cat{"Felix"}); + + BOOST_TEST(name(std::move(spot)) == "Spot the dog"); + BOOST_TEST(name(std::move(felix)) == "Felix the cat"); +} +} // namespace BOOST_OPENMETHOD_GENSYM +#endif \ No newline at end of file From f0aafd3d2c0148bf85b9085fdddb826c6d37ea37 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 7 Mar 2026 12:41:09 -0500 Subject: [PATCH 03/64] inter-operate with 'any' --- include/boost/openmethod/interop/std_any.hpp | 2 +- test/test_dispatch_std_any.cpp | 21 +++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/include/boost/openmethod/interop/std_any.hpp b/include/boost/openmethod/interop/std_any.hpp index 89af1e59..15267772 100644 --- a/include/boost/openmethod/interop/std_any.hpp +++ b/include/boost/openmethod/interop/std_any.hpp @@ -77,7 +77,7 @@ struct virtual_traits { //! @param arg An rvalue reference to the `std::any` method argument. //! @return The value stored in `arg`, cast to `U`. template - static auto cast(std::any&& arg) -> decltype(auto) { + static auto cast(const std::any& arg) { return std::any_cast(arg); } }; diff --git a/test/test_dispatch_std_any.cpp b/test/test_dispatch_std_any.cpp index 258b31b8..b0b7667b 100644 --- a/test/test_dispatch_std_any.cpp +++ b/test/test_dispatch_std_any.cpp @@ -22,7 +22,7 @@ using namespace boost::openmethod; \ use_any_types BOOST_OPENMETHOD_GENSYM; -#if 0 +#if 1 namespace BOOST_OPENMETHOD_GENSYM { @@ -37,15 +37,26 @@ BOOST_OPENMETHOD_OVERRIDE(name, (Dog dog), std::string) { return dog.name + " the dog"; } -BOOST_OPENMETHOD_OVERRIDE(name, (Cat cat), std::string) { - return cat.name + " the cat"; +BOOST_OPENMETHOD_OVERRIDE(name, (std::string name), std::string) { + return name; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (int value), std::string) { + std::ostringstream os; + os << value << " the integer"; + return os.str(); } BOOST_AUTO_TEST_CASE(std_any_by_value) { initialize(); - BOOST_TEST(name(std::any(Dog{"Spot"})) == "Spot the dog"); - BOOST_TEST(name(std::any(Cat{"Felix"})) == "Felix the cat"); + const std::any spot(Dog{"Spot"}); + const std::any felix(std::string{"Felix the cat"}); + const std::any answer(42); + + BOOST_TEST(name(spot) == "Spot the dog"); + BOOST_TEST(name(felix) == "Felix the cat"); + BOOST_TEST(name(answer) == "42 the integer"); } } // namespace BOOST_OPENMETHOD_GENSYM #endif From 491405325a41fc3585ae9825913402e08efa8f84 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Fri, 31 Jul 2026 04:35:43 -0400 Subject: [PATCH 04/64] support std::any by mutable and xvalue reference Add virtual_traits, and test dispatch on a std::any passed by mutable lvalue reference and by xvalue reference. virtual_ silently bound the generic virtual_traits, whose cast goes through optimal_cast - a static_cast/dynamic_cast that cannot compile against an overrider taking a reference to the contained type. Add a specialization with the full member set. virtual_traits::cast passed its parameter to std::any_cast as an lvalue, selecting the any_cast(any&) overload, which asserts is_constructible_v - false for an rvalue reference U. Forward it as an rvalue so any_cast(any&&) is selected. Also fix dynamic_vptr in that same specialization: it named the rtti policy, which has no type_vptr, and passed a type_info by value where a type_id is wanted. It compiles today only because acquire_vptr normalizes every reference category to const& before looking dynamic_vptr up, so the body is never instantiated. The mutable reference overriders cannot use BOOST_OPENMETHOD_OVERRIDE: the macro locates the method by checking that the overrider's parameter types can be passed to the method's forwarder, and nothing converts to a mutable lvalue reference to std::any. Register them via method<...>::override instead. Co-Authored-By: Claude Opus 5 (1M context) --- include/boost/openmethod/interop/std_any.hpp | 56 ++++----- test/test_dispatch_std_any.cpp | 119 +++++++++++++------ 2 files changed, 110 insertions(+), 65 deletions(-) diff --git a/include/boost/openmethod/interop/std_any.hpp b/include/boost/openmethod/interop/std_any.hpp index 15267772..b02df1a5 100644 --- a/include/boost/openmethod/interop/std_any.hpp +++ b/include/boost/openmethod/interop/std_any.hpp @@ -12,9 +12,6 @@ namespace boost::openmethod { namespace detail { -template -struct validate_method_parameter, Registry, void> - : std::true_type {}; template struct validate_method_parameter, Registry, void> @@ -30,15 +27,14 @@ struct validate_method_parameter, Registry, void> } // namespace detail -//! Specialize virtual_traits for std::any by value. +//! Specialize virtual_traits for `const std::any&` (const reference). //! //! Dispatch is based on the runtime type of the value stored in the `any`, -//! obtained via `std::any::type()`. Requires the registry to use a @ref -//! rtti policy that provides `dynamic_type` (e.g. @ref std_rtti). +//! obtained via `std::any::type()`. //! //! @tparam Registry A @ref registry. template -struct virtual_traits { +struct virtual_traits { //! The type used for dispatch. using virtual_type = std::any; @@ -64,29 +60,33 @@ struct virtual_traits { //! terminates the program with @ref abort. //! //! @param arg A reference to a const `any`. - //! @return A reference to a the v-table pointer for `Class`. + //! @return A reference to the v-table pointer for the stored value. static auto dynamic_vptr(const std::any& arg) -> const vptr_type& { - return Registry::rtti::type_vptr(arg.type()); - }; + return Registry::vptr::type_vptr(&arg.type()); + } //! Cast to a type. //! - //! Extracts the stored value using `std::any_cast`. + //! Extracts the stored value using `std::any_cast`. Since the `any` + //! argument is const, `U` cannot be a mutable reference. //! - //! @tparam U The target type (e.g. `Dog&`, `const Dog&`, `Dog`). - //! @param arg An rvalue reference to the `std::any` method argument. + //! @tparam U The target type (e.g. `const Dog&`, `Dog`). + //! @param arg A reference to a const `std::any` method argument. //! @return The value stored in `arg`, cast to `U`. template - static auto cast(const std::any& arg) { + static auto cast(const std::any& arg) -> decltype(auto) { return std::any_cast(arg); } }; //! Specialize virtual_traits for `std::any&` (mutable reference). //! +//! Dispatch is based on the runtime type of the value stored in the `any`, +//! obtained via `std::any::type()`. +//! //! @tparam Registry A @ref registry. template -struct virtual_traits { +struct virtual_traits { //! The type used for dispatch. using virtual_type = std::any; @@ -99,8 +99,8 @@ struct virtual_traits { //! Returns a *reference* to a v-table pointer for an object. //! - //! Acquires the dynamic @ref type_id of `arg`, using the registry's - //! @ref rtti policy. + //! Acquires the @ref type_id of the value stored in `arg`, using + //! `std::any::type()`. //! //! If the registry has a @ref type_hash policy, uses it to convert the //! type id to an index; otherwise, uses the type_id as the index. @@ -111,31 +111,31 @@ struct virtual_traits { //! its @ref error function with a @ref missing_class value, then //! terminates the program with @ref abort. //! - //! @param arg A reference to a const `any`. - //! @return A reference to a the v-table pointer for `Class`. + //! @param arg A reference to a `std::any`. + //! @return A reference to the v-table pointer for the stored value. static auto dynamic_vptr(const std::any& arg) -> const vptr_type& { return Registry::vptr::type_vptr(&arg.type()); - }; + } //! Cast to a type. //! //! Extracts the stored value using `std::any_cast`. Supports mutable - //! references (e.g. `Dog&`) because the `any` argument is non-const. + //! references (e.g. `Dog&`) because the `any` argument is not const; + //! modifications through the result are visible through the `any`. //! //! @tparam U The target type (e.g. `Dog&`, `const Dog&`, `Dog`). //! @param arg A mutable reference to the `std::any` method argument. //! @return The value stored in `arg`, cast to `U`. template - static auto cast(const std::any& arg) -> decltype(auto) { + static auto cast(std::any& arg) -> decltype(auto) { return std::any_cast(arg); } }; -//! Specialize virtual_traits for std::any by value. +//! Specialize virtual_traits for `std::any&&` (xvalue reference). //! //! Dispatch is based on the runtime type of the value stored in the `any`, -//! obtained via `std::any::type()`. Requires the registry to use a @ref -//! rtti policy that provides `dynamic_type` (e.g. @ref std_rtti). +//! obtained via `std::any::type()`. //! //! @tparam Registry A @ref registry. template @@ -167,8 +167,8 @@ struct virtual_traits { //! @param arg A reference to a const `any`. //! @return A reference to a the v-table pointer for `Class`. static auto dynamic_vptr(const std::any& arg) -> const vptr_type& { - return Registry::rtti::type_vptr(arg.type()); - }; + return Registry::vptr::type_vptr(&arg.type()); + } //! Cast to a type. //! @@ -179,7 +179,7 @@ struct virtual_traits { //! @return The value stored in `arg`, cast to `U`. template static auto cast(std::any&& arg) -> decltype(auto) { - return std::any_cast(arg); + return std::any_cast(std::move(arg)); } }; diff --git a/test/test_dispatch_std_any.cpp b/test/test_dispatch_std_any.cpp index b0b7667b..f746dbe1 100644 --- a/test/test_dispatch_std_any.cpp +++ b/test/test_dispatch_std_any.cpp @@ -22,33 +22,34 @@ using namespace boost::openmethod; \ use_any_types BOOST_OPENMETHOD_GENSYM; -#if 1 - namespace BOOST_OPENMETHOD_GENSYM { // ----------------------------------------------------------------------------- -// pass virtual args as std::any by value +// pass virtual args as const std::any& (const ref) + +static_assert(detail::has_dynamic_vptr< + virtual_traits, type_id>); MAKE_CLASSES(); -BOOST_OPENMETHOD(name, (virtual_), std::string); +BOOST_OPENMETHOD(name, (virtual_), std::string); -BOOST_OPENMETHOD_OVERRIDE(name, (Dog dog), std::string) { +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { return dog.name + " the dog"; } -BOOST_OPENMETHOD_OVERRIDE(name, (std::string name), std::string) { +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { return name; } -BOOST_OPENMETHOD_OVERRIDE(name, (int value), std::string) { +BOOST_OPENMETHOD_OVERRIDE(name, (const int& value), std::string) { std::ostringstream os; os << value << " the integer"; return os.str(); } -BOOST_AUTO_TEST_CASE(std_any_by_value) { - initialize(); +BOOST_AUTO_TEST_CASE(std_any_by_const_ref) { + initialize(trace()); const std::any spot(Dog{"Spot"}); const std::any felix(std::string{"Felix the cat"}); @@ -59,71 +60,115 @@ BOOST_AUTO_TEST_CASE(std_any_by_value) { BOOST_TEST(name(answer) == "42 the integer"); } } // namespace BOOST_OPENMETHOD_GENSYM -#endif + namespace BOOST_OPENMETHOD_GENSYM { // ----------------------------------------------------------------------------- -// pass virtual args as const std::any& (const ref) +// pass virtual args as std::any& (mutable ref) static_assert(detail::has_dynamic_vptr< - virtual_traits, type_id>); + virtual_traits, type_id>); MAKE_CLASSES(); -BOOST_OPENMETHOD(name, (virtual_), std::string); +BOOST_OPENMETHOD(bump, (virtual_), std::string); -BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { +// BOOST_OPENMETHOD_OVERRIDE cannot express this. It locates the method by +// checking that the overrider's parameter types can be passed to the method's +// forwarder (see enable_forwarder and the guide function in macros.hpp), and +// `Dog&` does not convert to `std::any&`. A temporary `std::any` binds to +// `const std::any&` and to `std::any&&`, which is why the other two reference +// categories can use the macro; nothing binds to a mutable lvalue reference. +// Register directly via method<...>::override instead - the primitive the +// macro itself expands to. + +using bump_method = + BOOST_OPENMETHOD_TYPE(bump, (virtual_), std::string); + +auto bump_dog(Dog& dog) -> std::string { + dog.name += " Jr."; return dog.name + " the dog"; } -BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { +auto bump_string(std::string& name) -> std::string { + name += "!"; return name; } -BOOST_OPENMETHOD_OVERRIDE(name, (const int& value), std::string) { +auto bump_int(int& value) -> std::string { + ++value; std::ostringstream os; os << value << " the integer"; return os.str(); } -BOOST_AUTO_TEST_CASE(std_any_by_const_ref) { +BOOST_OPENMETHOD_REGISTER(bump_method::override); +BOOST_OPENMETHOD_REGISTER(bump_method::override); +BOOST_OPENMETHOD_REGISTER(bump_method::override); + +BOOST_AUTO_TEST_CASE(std_any_by_mutable_ref) { initialize(trace()); - const std::any spot(Dog{"Spot"}); - const std::any felix(std::string{"Felix the cat"}); - const std::any answer(42); + std::any spot(Dog{"Spot"}); + std::any felix(std::string{"Felix the cat"}); + std::any answer(41); - BOOST_TEST(name(spot) == "Spot the dog"); - BOOST_TEST(name(felix) == "Felix the cat"); - BOOST_TEST(name(answer) == "42 the integer"); + BOOST_TEST(bump(spot) == "Spot Jr. the dog"); + BOOST_TEST(std::any_cast(spot).name == "Spot Jr."); + + BOOST_TEST(bump(felix) == "Felix the cat!"); + BOOST_TEST(std::any_cast(felix) == "Felix the cat!"); + + BOOST_TEST(bump(answer) == "42 the integer"); + BOOST_TEST(std::any_cast(answer) == 42); } } // namespace BOOST_OPENMETHOD_GENSYM -#if 0 + namespace BOOST_OPENMETHOD_GENSYM { // ----------------------------------------------------------------------------- -// pass virtual args as std::any&& (rvalue ref, move semantics) +// pass virtual args as std::any&& (xvalue ref) + +static_assert(detail::has_dynamic_vptr< + virtual_traits, type_id>); MAKE_CLASSES(); -BOOST_OPENMETHOD(name, (virtual_), std::string); +BOOST_OPENMETHOD(steal, (virtual_), std::string); -BOOST_OPENMETHOD_OVERRIDE(name, (Dog dog), std::string) { - return dog.name + " the dog"; +BOOST_OPENMETHOD_OVERRIDE(steal, (Dog && dog), std::string) { + Dog stolen(std::move(dog)); + return stolen.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(steal, (std::string && name), std::string) { + std::string stolen(std::move(name)); + return stolen; } -BOOST_OPENMETHOD_OVERRIDE(name, (Cat cat), std::string) { - return cat.name + " the cat"; +BOOST_OPENMETHOD_OVERRIDE(steal, (int&& value), std::string) { + std::ostringstream os; + os << value << " the integer"; + return os.str(); } -BOOST_AUTO_TEST_CASE(std_any_by_rvalue_ref) { - initialize(); +BOOST_AUTO_TEST_CASE(std_any_by_xvalue_ref) { + initialize(trace()); std::any spot(Dog{"Spot"}); - std::any felix(Cat{"Felix"}); - - BOOST_TEST(name(std::move(spot)) == "Spot the dog"); - BOOST_TEST(name(std::move(felix)) == "Felix the cat"); + BOOST_TEST(steal(std::move(spot)) == "Spot the dog"); + // the overrider moved the name out; the `any` still owns the Dog + BOOST_TEST(spot.has_value()); + BOOST_TEST(std::any_cast(spot).name == ""); + + std::any felix(std::string{"Felix the cat"}); + BOOST_TEST(steal(std::move(felix)) == "Felix the cat"); + BOOST_TEST(felix.has_value()); + BOOST_TEST(std::any_cast(felix) == ""); + + // moving an int copies it + std::any answer(42); + BOOST_TEST(steal(std::move(answer)) == "42 the integer"); + BOOST_TEST(std::any_cast(answer) == 42); } } // namespace BOOST_OPENMETHOD_GENSYM -#endif \ No newline at end of file From 810a9816734d80344a6b0b9f7169b4ea9f592f54 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Fri, 31 Jul 2026 05:57:44 -0400 Subject: [PATCH 05/64] support boost::any Add interop/boost_any.hpp, mirroring interop/std_any.hpp: virtual_traits specializations for const boost::any&, boost::any& and boost::any&&, and a use_boost_any_types registrar. Dispatch is on the type of the contained value, obtained from boost::any::type(), which yields the same std::type_info object std_rtti keys on. boost::any_cast is looser than std::any_cast. Its any& overload is unconstrained, so it binds an rvalue reference to the value held in an lvalue any - letting an overrider move out of an any the caller still owns - and its const any& overload fails inside Boost.Any rather than at the trait. Constrain cast with SFINAE in all three specializations, so the bad instantiations are removed from the overload set instead. Two compile_fail tests cover them; the diagnostic is the compiler's own overload resolution failure, whose wording varies, hence the loose fail_regex. Rename use_any_types to use_std_any_types, for symmetry with use_boost_any_types. One registrar cannot serve both: it names the any type twice, as the root class and as the synthetic base of the contained types, and that root must be the class the method registers for its virtual parameter. Boost.Any is not in the transitive closure of the library's declared dependencies, so declare it in the test Jamfile, and in CMakeLists.txt alongside Boost::smart_ptr - the mrdocs build compiles every header. Also document both any headers in ref_headers.adoc; std_any.hpp was missed when it landed. Co-Authored-By: Claude Opus 5 (1M context) --- CMakeLists.txt | 1 + doc/modules/ROOT/pages/ref_headers.adoc | 16 ++ .../boost/openmethod/interop/boost_any.hpp | 243 ++++++++++++++++++ include/boost/openmethod/interop/std_any.hpp | 22 +- test/CMakeLists.txt | 8 + test/Jamfile | 1 + ...ail_boost_any_const_ref_to_mutable_ref.cpp | 31 +++ ...il_boost_any_mutable_ref_to_rvalue_ref.cpp | 41 +++ test/test_dispatch_boost_any.cpp | 174 +++++++++++++ test/test_dispatch_std_any.cpp | 2 +- 10 files changed, 532 insertions(+), 7 deletions(-) create mode 100644 include/boost/openmethod/interop/boost_any.hpp create mode 100644 test/compile_fail_boost_any_const_ref_to_mutable_ref.cpp create mode 100644 test/compile_fail_boost_any_mutable_ref_to_rvalue_ref.cpp create mode 100644 test/test_dispatch_boost_any.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 50e246ba..193b3939 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -94,6 +94,7 @@ set( if (BOOST_OPENMETHOD_BUILD_TESTS OR BOOST_OPENMETHOD_MRDOCS_BUILD) list(APPEND BOOST_OPENMETHOD_DEPENDENCIES Boost::smart_ptr) + list(APPEND BOOST_OPENMETHOD_DEPENDENCIES Boost::any) endif() foreach (BOOST_OPENMETHOD_DEPENDENCY ${BOOST_OPENMETHOD_DEPENDENCIES}) diff --git a/doc/modules/ROOT/pages/ref_headers.adoc b/doc/modules/ROOT/pages/ref_headers.adoc index 0c89651a..4089b03e 100644 --- a/doc/modules/ROOT/pages/ref_headers.adoc +++ b/doc/modules/ROOT/pages/ref_headers.adoc @@ -71,6 +71,22 @@ Provides a `virtual_traits` specialization that makes it possible to use a Provides a `virtual_traits` specialization that makes it possible to use a `boost::intrusive_ptr` in place of a raw pointer or reference in virtual parameters. +[#std_any] +### link:{{BASE_URL}}/include/boost/openmethod/interop/std_any.hpp[] + +Provides `virtual_traits` specializations that make it possible to use a `std::any` - +by const reference, by mutable reference, or by rvalue reference - in virtual +parameters. Dispatch is on the type of the contained value. Also provides +`use_std_any_types`, which registers the types that may be contained. + +[#boost_any] +### link:{{BASE_URL}}/include/boost/openmethod/interop/boost_any.hpp[] + +Provides `virtual_traits` specializations that make it possible to use a `boost::any` - +by const reference, by mutable reference, or by rvalue reference - in virtual +parameters. Dispatch is on the type of the contained value. Also provides +`use_boost_any_types`, which registers the types that may be contained. + *The headers below are for advanced use*. ## Pre-Core Headers diff --git a/include/boost/openmethod/interop/boost_any.hpp b/include/boost/openmethod/interop/boost_any.hpp new file mode 100644 index 00000000..75f834c4 --- /dev/null +++ b/include/boost/openmethod/interop/boost_any.hpp @@ -0,0 +1,243 @@ +// Copyright (c) 2018-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) + +#ifndef BOOST_OPENMETHOD_INTEROP_BOOST_ANY_HPP +#define BOOST_OPENMETHOD_INTEROP_BOOST_ANY_HPP + +#include +#include + +namespace boost::openmethod { + +namespace detail { + +template +struct validate_method_parameter, Registry, void> + : std::true_type {}; + +template +struct validate_method_parameter, Registry, void> + : std::true_type {}; + +template +struct validate_method_parameter, Registry, void> + : std::true_type {}; + +} // namespace detail + +//! Specialize virtual_traits for `const boost::any&` (const reference). +//! +//! Dispatch is based on the runtime type of the value stored in the `any`, +//! obtained via `boost::any::type()`. +//! +//! @tparam Registry A @ref registry. +template +struct virtual_traits { + //! The type used for dispatch. + using virtual_type = boost::any; + + //! Returns a const reference to the `any` argument. + //! @param arg A reference to a `boost::any`. + //! @return A const reference to `arg`. + static auto peek(const boost::any& arg) -> const boost::any& { + return arg; + } + + //! Returns a *reference* to a v-table pointer for an object. + //! + //! Acquires the dynamic @ref type_id of the value stored in `arg`, using + //! `boost::any::type()`. This requires the registry's @ref rtti policy to + //! identify classes by `&typeid(T)`, as @ref std_rtti does; + //! `boost::any::type()` yields the same `std::type_info` object, provided + //! Boost.TypeIndex uses `stl_type_index`. + //! + //! If the registry has a @ref type_hash policy, uses it to convert the + //! type id to an index; otherwise, uses the type_id as the index. + //! + //! If the registry contains the @ref runtime_checks policy, verifies + //! that the index falls within the limits of the vector. If it does + //! not, and if the registry contains a @ref error_handler policy, calls + //! its @ref error function with a @ref missing_class value, then + //! terminates the program with @ref abort. + //! + //! @param arg A reference to a const `any`. + //! @return A reference to the v-table pointer for the stored value. + static auto dynamic_vptr(const boost::any& arg) -> const vptr_type& { + return Registry::vptr::type_vptr(&arg.type()); + } + + //! Cast to a type. + //! + //! Extracts the stored value using `boost::any_cast`. + //! + //! Since the `any` argument is const, `U` cannot be a mutable reference. + //! `boost::any_cast` rewrites `U` to a const reference for a const `any`, + //! and would fail inside Boost.Any; this overload is removed from the + //! overload set instead. + //! + //! @tparam U The target type (e.g. `const Dog&`, `Dog`). + //! @param arg A reference to a const `boost::any` method argument. + //! @return The value stored in `arg`, cast to `U`. + template< + typename U, + typename = std::enable_if_t< + !std::is_reference_v || + std::is_const_v>>> + static auto cast(const boost::any& arg) -> decltype(auto) { + return boost::any_cast(arg); + } +}; + +//! Specialize virtual_traits for `boost::any&` (mutable reference). +//! +//! Dispatch is based on the runtime type of the value stored in the `any`, +//! obtained via `boost::any::type()`. +//! +//! @tparam Registry A @ref registry. +template +struct virtual_traits { + //! The type used for dispatch. + using virtual_type = boost::any; + + //! Returns a const reference to the `any` argument. + //! @param arg A reference to a `boost::any`. + //! @return A const reference to `arg`. + static auto peek(const boost::any& arg) -> const boost::any& { + return arg; + } + + //! Returns a *reference* to a v-table pointer for an object. + //! + //! Acquires the dynamic @ref type_id of the value stored in `arg`, using + //! `boost::any::type()`. This requires the registry's @ref rtti policy to + //! identify classes by `&typeid(T)`, as @ref std_rtti does; + //! `boost::any::type()` yields the same `std::type_info` object, provided + //! Boost.TypeIndex uses `stl_type_index`. + //! + //! If the registry has a @ref type_hash policy, uses it to convert the + //! type id to an index; otherwise, uses the type_id as the index. + //! + //! If the registry contains the @ref runtime_checks policy, verifies + //! that the index falls within the limits of the vector. If it does + //! not, and if the registry contains a @ref error_handler policy, calls + //! its @ref error function with a @ref missing_class value, then + //! terminates the program with @ref abort. + //! + //! @param arg A reference to a `boost::any`. + //! @return A reference to the v-table pointer for the stored value. + static auto dynamic_vptr(const boost::any& arg) -> const vptr_type& { + return Registry::vptr::type_vptr(&arg.type()); + } + + //! Cast to a type. + //! + //! Extracts the stored value using `boost::any_cast`. Supports mutable + //! references (e.g. `Dog&`) because the `any` argument is not const; + //! modifications through the result are visible through the `any`. + //! + //! `U` cannot be an rvalue reference. Unlike `std::any_cast`, + //! `boost::any_cast` binds an rvalue reference to the value stored in an + //! lvalue `any`; moving the value out must go through an explicit + //! `virtual_` parameter, so this overload is removed from + //! the overload set. + //! + //! @tparam U The target type (e.g. `Dog&`, `const Dog&`, `Dog`). + //! @param arg A mutable reference to the `boost::any` method argument. + //! @return The value stored in `arg`, cast to `U`. + template< + typename U, typename = std::enable_if_t>> + static auto cast(boost::any& arg) -> decltype(auto) { + return boost::any_cast(arg); + } +}; + +//! Specialize virtual_traits for `boost::any&&` (xvalue reference). +//! +//! Dispatch is based on the runtime type of the value stored in the `any`, +//! obtained via `boost::any::type()`. +//! +//! @tparam Registry A @ref registry. +template +struct virtual_traits { + //! The type used for dispatch. + using virtual_type = boost::any; + + //! Returns a const reference to the `any` argument. + //! @param arg A reference to a `boost::any`. + //! @return A const reference to `arg`. + static auto peek(const boost::any& arg) -> const boost::any& { + return arg; + } + + //! Returns a *reference* to a v-table pointer for an object. + //! + //! Acquires the dynamic @ref type_id of the value stored in `arg`, using + //! `boost::any::type()`. This requires the registry's @ref rtti policy to + //! identify classes by `&typeid(T)`, as @ref std_rtti does; + //! `boost::any::type()` yields the same `std::type_info` object, provided + //! Boost.TypeIndex uses `stl_type_index`. + //! + //! If the registry has a @ref type_hash policy, uses it to convert the + //! type id to an index; otherwise, uses the type_id as the index. + //! + //! If the registry contains the @ref runtime_checks policy, verifies + //! that the index falls within the limits of the vector. If it does + //! not, and if the registry contains a @ref error_handler policy, calls + //! its @ref error function with a @ref missing_class value, then + //! terminates the program with @ref abort. + //! + //! @param arg A reference to a `boost::any`. + //! @return A reference to the v-table pointer for the stored value. + static auto dynamic_vptr(const boost::any& arg) -> const vptr_type& { + return Registry::vptr::type_vptr(&arg.type()); + } + + //! Cast to a type. + //! + //! Extracts the stored value using `boost::any_cast`. + //! + //! `U` cannot be a mutable lvalue reference: that would bind a reference + //! to the value contained in a temporary. Boost.Any rejects it with a + //! static assertion; this overload is removed from the overload set + //! instead, for consistency with the other reference categories. + //! + //! @tparam U The target type (e.g. `Dog&&`, `const Dog&`, `Dog`). + //! @param arg An rvalue reference to the `boost::any` method argument. + //! @return The value stored in `arg`, cast to `U`. + template< + typename U, + typename = std::enable_if_t< + !std::is_lvalue_reference_v || + std::is_const_v>>> + static auto cast(boost::any&& arg) -> decltype(auto) { + return boost::any_cast(std::move(arg)); + } +}; + +//! Register the types that a `boost::any` virtual parameter may contain. +//! +//! Registers `boost::any` as a class, and each `T` as a class derived from +//! `boost::any`. This makes the contained types visible to the dispatch +//! machinery, which resolves a call on the `type_id` returned by +//! `boost::any::type()`. +//! +//! The root class is `boost::any`, distinct from the one used by +//! @ref use_std_any_types for `std::any`, so both may be used in the same +//! program, and with the same registry. +//! +//! @tparam T... The types that may be stored in the `any`, optionally +//! followed by a @ref registry. +template +struct use_boost_any_types + : detail::use_class_aux< + typename detail::extract_registry::registry, + mp11::mp_list>, + detail::use_class_aux< + typename detail::extract_registry::registry, + mp11::mp_list>... {}; + +} // namespace boost::openmethod + +#endif diff --git a/include/boost/openmethod/interop/std_any.hpp b/include/boost/openmethod/interop/std_any.hpp index b02df1a5..923e56c1 100644 --- a/include/boost/openmethod/interop/std_any.hpp +++ b/include/boost/openmethod/interop/std_any.hpp @@ -183,13 +183,23 @@ struct virtual_traits { } }; +//! Register the types that a `std::any` virtual parameter may contain. +//! +//! Registers `std::any` as a class, and each `T` as a class derived from +//! `std::any`. This makes the contained types visible to the dispatch +//! machinery, which resolves a call on the `type_id` returned by +//! `std::any::type()`. +//! +//! @tparam T... The types that may be stored in the `any`, optionally +//! followed by a @ref registry. template -struct use_any_types : detail::use_class_aux< - typename detail::extract_registry::registry, - mp11::mp_list>, - detail::use_class_aux< - typename detail::extract_registry::registry, - mp11::mp_list>... {}; +struct use_std_any_types + : detail::use_class_aux< + typename detail::extract_registry::registry, + mp11::mp_list>, + detail::use_class_aux< + typename detail::extract_registry::registry, + mp11::mp_list>... {}; } // namespace boost::openmethod diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index f9f4524b..ed1f9f77 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -155,6 +155,14 @@ openmethod_compile_fail_test( compile_fail_repeated_inheritance "repeated inheritance") openmethod_compile_fail_test( compile_fail_override_method_not_found "cannot find 'speak' method that accepts the same arguments as the overrider") +# The constrained `cast` is removed from the overload set, so the diagnostic is +# the compiler's own overload resolution failure, whose wording varies: "no +# matching function for call to" on clang and gcc, "no matching overloaded +# function found" on MSVC. +openmethod_compile_fail_test( + compile_fail_boost_any_const_ref_to_mutable_ref "no matching") +openmethod_compile_fail_test( + compile_fail_boost_any_mutable_ref_to_rvalue_ref "no matching") if (TARGET Boost::dll) add_subdirectory(dynamic_loading) diff --git a/test/Jamfile b/test/Jamfile index a1c69c4e..10ab8c56 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -20,6 +20,7 @@ project cxx17_structured_bindings ] /boost/openmethod//boost_openmethod + /boost/any//boost_any extra diff --git a/test/compile_fail_boost_any_const_ref_to_mutable_ref.cpp b/test/compile_fail_boost_any_const_ref_to_mutable_ref.cpp new file mode 100644 index 00000000..99058c55 --- /dev/null +++ b/test/compile_fail_boost_any_const_ref_to_mutable_ref.cpp @@ -0,0 +1,31 @@ +// Copyright (c) 2018-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +#include + +#include +#include + +using namespace boost::openmethod; + +struct Dog { + std::string name; +}; + +BOOST_OPENMETHOD_REGISTER(use_boost_any_types); + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +// The `any` is const, so boost::any_cast cannot produce a mutable reference to +// the value it contains. Without the constraint on `cast`, this would fail +// inside Boost.Any instead of at the trait. +BOOST_OPENMETHOD_OVERRIDE(name, (Dog & dog), std::string) { + return dog.name; +} + +int main() { + return 0; +} diff --git a/test/compile_fail_boost_any_mutable_ref_to_rvalue_ref.cpp b/test/compile_fail_boost_any_mutable_ref_to_rvalue_ref.cpp new file mode 100644 index 00000000..d738e2d2 --- /dev/null +++ b/test/compile_fail_boost_any_mutable_ref_to_rvalue_ref.cpp @@ -0,0 +1,41 @@ +// Copyright (c) 2018-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +#include + +#include +#include + +using namespace boost::openmethod; + +struct Dog { + std::string name; +}; + +BOOST_OPENMETHOD_REGISTER(use_boost_any_types); + +BOOST_OPENMETHOD(bump, (virtual_), std::string); + +using bump_method = + BOOST_OPENMETHOD_TYPE(bump, (virtual_), std::string); + +// Unlike std::any_cast, boost::any_cast binds an rvalue reference to the value +// stored in an lvalue `any`, which would let this overrider move the value out +// of an `any` the caller still owns. Moving the value out must go through a +// virtual_ parameter. +// +// The overrider is registered via method<...>::override because +// BOOST_OPENMETHOD_OVERRIDE cannot locate a method whose virtual parameter is +// a mutable lvalue reference to `any` - see test_dispatch_boost_any.cpp. +auto bump_dog(Dog&& dog) -> std::string { + return std::move(dog.name); +} + +BOOST_OPENMETHOD_REGISTER(bump_method::override); + +int main() { + return 0; +} diff --git a/test/test_dispatch_boost_any.cpp b/test/test_dispatch_boost_any.cpp new file mode 100644 index 00000000..ede5d029 --- /dev/null +++ b/test/test_dispatch_boost_any.cpp @@ -0,0 +1,174 @@ +// Copyright (c) 2018-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +#include + +#include +#include +#include + +#define BOOST_TEST_MODULE dispatch_boost_any +#include + +using namespace boost::openmethod; + +#define MAKE_CLASSES() \ + struct Dog { \ + std::string name; \ + }; \ + \ + use_boost_any_types BOOST_OPENMETHOD_GENSYM; + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as const boost::any& (const ref) + +static_assert(detail::has_dynamic_vptr< + virtual_traits, type_id>); + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { + return name; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const int& value), std::string) { + std::ostringstream os; + os << value << " the integer"; + return os.str(); +} + +BOOST_AUTO_TEST_CASE(boost_any_by_const_ref) { + initialize(trace()); + + const boost::any spot(Dog{"Spot"}); + const boost::any felix(std::string{"Felix the cat"}); + const boost::any answer(42); + + BOOST_TEST(name(spot) == "Spot the dog"); + BOOST_TEST(name(felix) == "Felix the cat"); + BOOST_TEST(name(answer) == "42 the integer"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as boost::any& (mutable ref) + +static_assert(detail::has_dynamic_vptr< + virtual_traits, type_id>); + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(bump, (virtual_), std::string); + +// BOOST_OPENMETHOD_OVERRIDE cannot express this. It locates the method by +// checking that the overrider's parameter types can be passed to the method's +// forwarder (see enable_forwarder and the guide function in macros.hpp), and +// `Dog&` does not convert to `boost::any&`. A temporary `boost::any` binds to +// `const boost::any&` and to `boost::any&&`, which is why the other two +// reference categories can use the macro; nothing binds to a mutable lvalue +// reference. Register directly via method<...>::override instead - the +// primitive the macro itself expands to. + +using bump_method = + BOOST_OPENMETHOD_TYPE(bump, (virtual_), std::string); + +auto bump_dog(Dog& dog) -> std::string { + dog.name += " Jr."; + return dog.name + " the dog"; +} + +auto bump_string(std::string& name) -> std::string { + name += "!"; + return name; +} + +auto bump_int(int& value) -> std::string { + ++value; + std::ostringstream os; + os << value << " the integer"; + return os.str(); +} + +BOOST_OPENMETHOD_REGISTER(bump_method::override); +BOOST_OPENMETHOD_REGISTER(bump_method::override); +BOOST_OPENMETHOD_REGISTER(bump_method::override); + +BOOST_AUTO_TEST_CASE(boost_any_by_mutable_ref) { + initialize(trace()); + + boost::any spot(Dog{"Spot"}); + boost::any felix(std::string{"Felix the cat"}); + boost::any answer(41); + + BOOST_TEST(bump(spot) == "Spot Jr. the dog"); + BOOST_TEST(boost::any_cast(spot).name == "Spot Jr."); + + BOOST_TEST(bump(felix) == "Felix the cat!"); + BOOST_TEST(boost::any_cast(felix) == "Felix the cat!"); + + BOOST_TEST(bump(answer) == "42 the integer"); + BOOST_TEST(boost::any_cast(answer) == 42); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as boost::any&& (xvalue ref) + +static_assert(detail::has_dynamic_vptr< + virtual_traits, type_id>); + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(steal, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(steal, (Dog && dog), std::string) { + Dog stolen(std::move(dog)); + return stolen.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(steal, (std::string && name), std::string) { + std::string stolen(std::move(name)); + return stolen; +} + +BOOST_OPENMETHOD_OVERRIDE(steal, (int&& value), std::string) { + std::ostringstream os; + os << value << " the integer"; + return os.str(); +} + +BOOST_AUTO_TEST_CASE(boost_any_by_xvalue_ref) { + initialize(trace()); + + boost::any spot(Dog{"Spot"}); + BOOST_TEST(steal(std::move(spot)) == "Spot the dog"); + // the overrider moved the name out; the `any` still owns the Dog + BOOST_TEST(!spot.empty()); + BOOST_TEST(boost::any_cast(spot).name == ""); + + boost::any felix(std::string{"Felix the cat"}); + BOOST_TEST(steal(std::move(felix)) == "Felix the cat"); + BOOST_TEST(!felix.empty()); + BOOST_TEST(boost::any_cast(felix) == ""); + + // moving an int copies it + boost::any answer(42); + BOOST_TEST(steal(std::move(answer)) == "42 the integer"); + BOOST_TEST(boost::any_cast(answer) == 42); +} +} // namespace BOOST_OPENMETHOD_GENSYM diff --git a/test/test_dispatch_std_any.cpp b/test/test_dispatch_std_any.cpp index f746dbe1..0bc1c12d 100644 --- a/test/test_dispatch_std_any.cpp +++ b/test/test_dispatch_std_any.cpp @@ -20,7 +20,7 @@ using namespace boost::openmethod; std::string name; \ }; \ \ - use_any_types BOOST_OPENMETHOD_GENSYM; + use_std_any_types BOOST_OPENMETHOD_GENSYM; namespace BOOST_OPENMETHOD_GENSYM { From f086985e63ad5662f3218ef9adca493d031385cf Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 2 Aug 2026 11:31:48 -0400 Subject: [PATCH 06/64] type_vptr -> vptr --- include/boost/openmethod/interop/boost_any.hpp | 6 +++--- include/boost/openmethod/interop/std_any.hpp | 6 +++--- include/boost/openmethod/policies/vptr_map.hpp | 4 ++-- include/boost/openmethod/policies/vptr_vector.hpp | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/include/boost/openmethod/interop/boost_any.hpp b/include/boost/openmethod/interop/boost_any.hpp index 75f834c4..0bff558d 100644 --- a/include/boost/openmethod/interop/boost_any.hpp +++ b/include/boost/openmethod/interop/boost_any.hpp @@ -65,7 +65,7 @@ struct virtual_traits { //! @param arg A reference to a const `any`. //! @return A reference to the v-table pointer for the stored value. static auto dynamic_vptr(const boost::any& arg) -> const vptr_type& { - return Registry::vptr::type_vptr(&arg.type()); + return Registry::vptr::vptr(&arg.type()); } //! Cast to a type. @@ -128,7 +128,7 @@ struct virtual_traits { //! @param arg A reference to a `boost::any`. //! @return A reference to the v-table pointer for the stored value. static auto dynamic_vptr(const boost::any& arg) -> const vptr_type& { - return Registry::vptr::type_vptr(&arg.type()); + return Registry::vptr::vptr(&arg.type()); } //! Cast to a type. @@ -191,7 +191,7 @@ struct virtual_traits { //! @param arg A reference to a `boost::any`. //! @return A reference to the v-table pointer for the stored value. static auto dynamic_vptr(const boost::any& arg) -> const vptr_type& { - return Registry::vptr::type_vptr(&arg.type()); + return Registry::vptr::vptr(&arg.type()); } //! Cast to a type. diff --git a/include/boost/openmethod/interop/std_any.hpp b/include/boost/openmethod/interop/std_any.hpp index 923e56c1..ce5b6515 100644 --- a/include/boost/openmethod/interop/std_any.hpp +++ b/include/boost/openmethod/interop/std_any.hpp @@ -62,7 +62,7 @@ struct virtual_traits { //! @param arg A reference to a const `any`. //! @return A reference to the v-table pointer for the stored value. static auto dynamic_vptr(const std::any& arg) -> const vptr_type& { - return Registry::vptr::type_vptr(&arg.type()); + return Registry::vptr::vptr(&arg.type()); } //! Cast to a type. @@ -114,7 +114,7 @@ struct virtual_traits { //! @param arg A reference to a `std::any`. //! @return A reference to the v-table pointer for the stored value. static auto dynamic_vptr(const std::any& arg) -> const vptr_type& { - return Registry::vptr::type_vptr(&arg.type()); + return Registry::vptr::vptr(&arg.type()); } //! Cast to a type. @@ -167,7 +167,7 @@ struct virtual_traits { //! @param arg A reference to a const `any`. //! @return A reference to a the v-table pointer for `Class`. static auto dynamic_vptr(const std::any& arg) -> const vptr_type& { - return Registry::vptr::type_vptr(&arg.type()); + return Registry::vptr::vptr(&arg.type()); } //! Cast to a type. diff --git a/include/boost/openmethod/policies/vptr_map.hpp b/include/boost/openmethod/policies/vptr_map.hpp index 677d5400..52cef07d 100644 --- a/include/boost/openmethod/policies/vptr_map.hpp +++ b/include/boost/openmethod/policies/vptr_map.hpp @@ -94,7 +94,7 @@ class vptr_map : public vptr { //! @return A reference to a the v-table pointer for `Class`. template static auto dynamic_vptr(const Class& arg) -> const vptr_type& { - return type_vptr(Registry::rtti::dynamic_type(arg)); + return vptr(Registry::rtti::dynamic_type(arg)); } //! Returns a *reference* to a v-table pointer for a type. @@ -107,7 +107,7 @@ class vptr_map : public vptr { //! //! @param type A `type_id`. //! @return A reference to a the v-table pointer for `type`. - static auto type_vptr(type_id type) -> const vptr_type& { + static auto vptr(type_id type) -> const vptr_type& { auto iter = st().vptrs.find(type); if constexpr (Registry::has_runtime_checks) { diff --git a/include/boost/openmethod/policies/vptr_vector.hpp b/include/boost/openmethod/policies/vptr_vector.hpp index 1494ac24..717a5ee4 100644 --- a/include/boost/openmethod/policies/vptr_vector.hpp +++ b/include/boost/openmethod/policies/vptr_vector.hpp @@ -150,7 +150,7 @@ struct vptr_vector : vptr { //! @return A reference to a the v-table pointer for `Class`. template static auto dynamic_vptr(const Class& arg) -> const vptr_type& { - return type_vptr(Registry::rtti::dynamic_type(arg)); + return vptr(Registry::rtti::dynamic_type(arg)); }; //! Returns a *reference* to a v-table pointer for a type. @@ -166,7 +166,7 @@ struct vptr_vector : vptr { //! //! @param type A `type_id`. //! @return A reference to a the v-table pointer for `type`. - static auto type_vptr(type_id type) -> const vptr_type& { + static auto vptr(type_id type) -> const vptr_type& { std::size_t index; if constexpr (has_type_hash) { index = type_hash::hash(type); From 7ecd96ce7c6b69bdc05351d0b7d6185ad15296f3 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Fri, 7 Aug 2026 20:39:53 -0400 Subject: [PATCH 07/64] dynamic_vptr -> vptr --- include/boost/openmethod/core.hpp | 33 ++++++++++++------- .../boost/openmethod/interop/boost_any.hpp | 6 ++-- include/boost/openmethod/interop/std_any.hpp | 6 ++-- test/test_dispatch_std_any.cpp | 6 ++-- 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index 1cc081ee..eefc6d9d 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -526,7 +526,7 @@ constexpr bool has_vptr_fn = std::is_same_v< std::declval(), std::declval())), vptr_type>; -BOOST_OPENMETHOD_DETAIL_HAS_STATIC_FN(dynamic_vptr); +BOOST_OPENMETHOD_DETAIL_HAS_STATIC_FN(vptr); template decltype(auto) acquire_vptr(const ArgType& arg) { @@ -534,13 +534,12 @@ decltype(auto) acquire_vptr(const ArgType& arg) { if constexpr (has_vptr_fn) { return boost_openmethod_vptr(arg, static_cast(nullptr)); - } else if constexpr (has_dynamic_vptr< + } else if constexpr (has_vptr< virtual_traits, type_id>) { - return virtual_traits::dynamic_vptr( - arg); + return virtual_traits::vptr(arg); } else { - return Registry::template policy::dynamic_vptr(arg); + return Registry::template policy::vptr(arg); } } @@ -2348,7 +2347,7 @@ class method void resolve_type_ids(); - template + template auto vptr(const ArgType& arg) const -> vptr_type; template @@ -2502,7 +2501,7 @@ method::operator()( typename BOOST_OPENMETHOD_DETAIL_UNLESS_MRDOCS StripVirtualDecorator::type... args) const -> ReturnType { using namespace detail; - auto pf = resolve(parameter_traits::peek(args)...); + auto pf = resolve(args...); return pf(std::forward::type>( args)...); @@ -2534,13 +2533,23 @@ BOOST_FORCEINLINE template< typename Id, typename... Parameters, typename ReturnType, class Registry> -template +template BOOST_FORCEINLINE auto method::vptr( const ArgType& arg) const -> vptr_type { if constexpr (detail::is_virtual_ptr) { return arg.vptr(); } else { - return detail::acquire_vptr(arg); + decltype(auto) obj = virtual_traits::peek(arg); + + if constexpr (detail::has_vptr_fn) { + return boost_openmethod_vptr(obj, static_cast(nullptr)); + } else if constexpr (detail::has_vptr< + virtual_traits, + type_id>) { + return virtual_traits::vptr(obj); + } else { + return Registry::template policy::dynamic_vptr(obj); + } } } @@ -2557,7 +2566,7 @@ method::resolve_uni( using namespace boost::mp11; if constexpr (is_virtual>::value) { - vptr_type vtbl = vptr(arg); + vptr_type vtbl = vptr>>(arg); return vtbl[this->slots_strides[0]]; } else { return resolve_uni>(more_args...); @@ -2576,7 +2585,7 @@ method::resolve_multi_first( using namespace boost::mp11; if constexpr (is_virtual>::value) { - vptr_type vtbl = vptr(arg); + vptr_type vtbl = vptr>>(arg); std::size_t slot = this->slots_strides[0]; // The first virtual parameter is special. Since its stride is @@ -2606,7 +2615,7 @@ method::resolve_multi_next( using namespace boost::mp11; if constexpr (is_virtual>::value) { - vptr_type vtbl = vptr(arg); + vptr_type vtbl = vptr>>(arg); std::size_t slot = this->slots_strides[VirtualArg]; std::size_t stride = this->slots_strides[Arity + VirtualArg - 1]; dispatch = dispatch + vtbl[slot].i * stride; diff --git a/include/boost/openmethod/interop/boost_any.hpp b/include/boost/openmethod/interop/boost_any.hpp index 0bff558d..6234e5b2 100644 --- a/include/boost/openmethod/interop/boost_any.hpp +++ b/include/boost/openmethod/interop/boost_any.hpp @@ -64,7 +64,7 @@ struct virtual_traits { //! //! @param arg A reference to a const `any`. //! @return A reference to the v-table pointer for the stored value. - static auto dynamic_vptr(const boost::any& arg) -> const vptr_type& { + static auto vptr(const boost::any& arg) -> const vptr_type& { return Registry::vptr::vptr(&arg.type()); } @@ -127,7 +127,7 @@ struct virtual_traits { //! //! @param arg A reference to a `boost::any`. //! @return A reference to the v-table pointer for the stored value. - static auto dynamic_vptr(const boost::any& arg) -> const vptr_type& { + static auto vptr(const boost::any& arg) -> const vptr_type& { return Registry::vptr::vptr(&arg.type()); } @@ -190,7 +190,7 @@ struct virtual_traits { //! //! @param arg A reference to a `boost::any`. //! @return A reference to the v-table pointer for the stored value. - static auto dynamic_vptr(const boost::any& arg) -> const vptr_type& { + static auto vptr(const boost::any& arg) -> const vptr_type& { return Registry::vptr::vptr(&arg.type()); } diff --git a/include/boost/openmethod/interop/std_any.hpp b/include/boost/openmethod/interop/std_any.hpp index ce5b6515..46c340ed 100644 --- a/include/boost/openmethod/interop/std_any.hpp +++ b/include/boost/openmethod/interop/std_any.hpp @@ -61,7 +61,7 @@ struct virtual_traits { //! //! @param arg A reference to a const `any`. //! @return A reference to the v-table pointer for the stored value. - static auto dynamic_vptr(const std::any& arg) -> const vptr_type& { + static auto vptr(const std::any& arg) -> const vptr_type& { return Registry::vptr::vptr(&arg.type()); } @@ -113,7 +113,7 @@ struct virtual_traits { //! //! @param arg A reference to a `std::any`. //! @return A reference to the v-table pointer for the stored value. - static auto dynamic_vptr(const std::any& arg) -> const vptr_type& { + static auto vptr(const std::any& arg) -> const vptr_type& { return Registry::vptr::vptr(&arg.type()); } @@ -166,7 +166,7 @@ struct virtual_traits { //! //! @param arg A reference to a const `any`. //! @return A reference to a the v-table pointer for `Class`. - static auto dynamic_vptr(const std::any& arg) -> const vptr_type& { + static auto vptr(const std::any& arg) -> const vptr_type& { return Registry::vptr::vptr(&arg.type()); } diff --git a/test/test_dispatch_std_any.cpp b/test/test_dispatch_std_any.cpp index 0bc1c12d..9c8e7025 100644 --- a/test/test_dispatch_std_any.cpp +++ b/test/test_dispatch_std_any.cpp @@ -27,7 +27,7 @@ namespace BOOST_OPENMETHOD_GENSYM { // ----------------------------------------------------------------------------- // pass virtual args as const std::any& (const ref) -static_assert(detail::has_dynamic_vptr< +static_assert(detail::has_vptr< virtual_traits, type_id>); MAKE_CLASSES(); @@ -66,7 +66,7 @@ namespace BOOST_OPENMETHOD_GENSYM { // ----------------------------------------------------------------------------- // pass virtual args as std::any& (mutable ref) -static_assert(detail::has_dynamic_vptr< +static_assert(detail::has_vptr< virtual_traits, type_id>); MAKE_CLASSES(); @@ -129,7 +129,7 @@ namespace BOOST_OPENMETHOD_GENSYM { // ----------------------------------------------------------------------------- // pass virtual args as std::any&& (xvalue ref) -static_assert(detail::has_dynamic_vptr< +static_assert(detail::has_vptr< virtual_traits, type_id>); MAKE_CLASSES(); From 65cd1b563a13288d081b0e6b67b1e1cbb45167d4 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 8 Aug 2026 11:47:18 -0400 Subject: [PATCH 08/64] fix leftovers of the dynamic_vptr -> vptr rename Commit 7ecd96c renamed acquire_vptr's registry-policy fallback from dynamic_vptr(arg) to vptr(arg), but the policies' object-taking overload is still named dynamic_vptr - vptr(type_id) is the id-taking one. The fallback is reached whenever a plain virtual_ptr is constructed from a reference or pointer to a polymorphic object, so every such construction failed to compile; stale incremental builds masked it. Restore dynamic_vptr, matching method::vptr's own fallback. Also update test_dispatch_boost_any.cpp's has_dynamic_vptr static_asserts to has_vptr; the rename had updated the std counterpart only. Co-Authored-By: Claude Fable 5 --- include/boost/openmethod/core.hpp | 2 +- test/test_dispatch_boost_any.cpp | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index eefc6d9d..180adc77 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -539,7 +539,7 @@ decltype(auto) acquire_vptr(const ArgType& arg) { type_id>) { return virtual_traits::vptr(arg); } else { - return Registry::template policy::vptr(arg); + return Registry::template policy::dynamic_vptr(arg); } } diff --git a/test/test_dispatch_boost_any.cpp b/test/test_dispatch_boost_any.cpp index ede5d029..bfd850db 100644 --- a/test/test_dispatch_boost_any.cpp +++ b/test/test_dispatch_boost_any.cpp @@ -27,7 +27,7 @@ namespace BOOST_OPENMETHOD_GENSYM { // ----------------------------------------------------------------------------- // pass virtual args as const boost::any& (const ref) -static_assert(detail::has_dynamic_vptr< +static_assert(detail::has_vptr< virtual_traits, type_id>); MAKE_CLASSES(); @@ -66,8 +66,8 @@ namespace BOOST_OPENMETHOD_GENSYM { // ----------------------------------------------------------------------------- // pass virtual args as boost::any& (mutable ref) -static_assert(detail::has_dynamic_vptr< - virtual_traits, type_id>); +static_assert( + detail::has_vptr, type_id>); MAKE_CLASSES(); @@ -129,8 +129,8 @@ namespace BOOST_OPENMETHOD_GENSYM { // ----------------------------------------------------------------------------- // pass virtual args as boost::any&& (xvalue ref) -static_assert(detail::has_dynamic_vptr< - virtual_traits, type_id>); +static_assert( + detail::has_vptr, type_id>); MAKE_CLASSES(); From 57b6c32b8040b34e82ce4ee6f130dbc8aa129e87 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 8 Aug 2026 11:47:32 -0400 Subject: [PATCH 09/64] add virtual_any virtual_any is to `any` what virtual_ptr is to a pointer: it combines an `any` - held by value - with the v-table pointer for the contained value, so methods dispatch on the contained type without looking it up on every call. The v-table pointer is acquired at construction: from the dynamic type of an existing `any` (a hash table lookup via virtual_traits::vptr), or statically when the contained type is known (the value constructor, emplace, and the make_*_virtual factories use static_vptr, like make_unique_virtual). Assignment and emplace re-derive it, and no mutable accessor to the `any` is exposed, so the vptr always matches the payload. Methods take virtual_any by const, mutable or rvalue reference; overriders receive the contained type by a reference of a compatible category - the casts delegate to the existing virtual_traits specializations - or the virtual_any itself, unchanged, for a catch-all overrider. Passing virtual_any by value is rejected: it would copy the payload on every call. The value constructor makes overrider parameters convertible to the method's, so BOOST_OPENMETHOD_OVERRIDE locates virtual_any methods; the mutable lvalue case still needs method<...>::override, as with virtual_. No changes to core.hpp: dispatch reads the stored vptr through the boost_openmethod_vptr hook (a friend, so ADL only finds it when a virtual_any is an argument), and the detail templates (is_virtual, parameter_traits, validate_method_parameter, validate_overrider_parameter, select_overrider_virtual_type_aux) are specialized on the concrete class. The exact-pair validate_overrider_parameter specializations disambiguate with the generic one, which partial ordering ranks neither above nor below . The class is generic: it only requires virtual_traits with vptr and cast, so it serves std::any, boost::any, and future any-likes. std_any.hpp and boost_any.hpp provide the default-registry aliases virtual_std_any and virtual_boost_any and the make_std_any_virtual and make_boost_any_virtual factories. They also delete the final_virtual_ptr overloads for their `any` type: the primary template would silently use static_vptr - the v-table of the `any` root class, not of the contained value. Co-Authored-By: Claude Fable 5 --- doc/modules/ROOT/pages/ref_headers.adoc | 26 +- .../boost/openmethod/interop/boost_any.hpp | 52 ++ include/boost/openmethod/interop/std_any.hpp | 52 ++ .../boost/openmethod/interop/virtual_any.hpp | 498 ++++++++++++++++++ test/CMakeLists.txt | 6 + ...compile_fail_final_virtual_ptr_std_any.cpp | 27 + test/compile_fail_virtual_any_by_value.cpp | 26 + test/test_virtual_any_boost.cpp | 244 +++++++++ test/test_virtual_any_std.cpp | 244 +++++++++ 9 files changed, 1173 insertions(+), 2 deletions(-) create mode 100644 include/boost/openmethod/interop/virtual_any.hpp create mode 100644 test/compile_fail_final_virtual_ptr_std_any.cpp create mode 100644 test/compile_fail_virtual_any_by_value.cpp create mode 100644 test/test_virtual_any_boost.cpp create mode 100644 test/test_virtual_any_std.cpp diff --git a/doc/modules/ROOT/pages/ref_headers.adoc b/doc/modules/ROOT/pages/ref_headers.adoc index 4089b03e..b8c89482 100644 --- a/doc/modules/ROOT/pages/ref_headers.adoc +++ b/doc/modules/ROOT/pages/ref_headers.adoc @@ -71,13 +71,31 @@ Provides a `virtual_traits` specialization that makes it possible to use a Provides a `virtual_traits` specialization that makes it possible to use a `boost::intrusive_ptr` in place of a raw pointer or reference in virtual parameters. +[#virtual_any] +### link:{{BASE_URL}}/include/boost/openmethod/interop/virtual_any.hpp[] + +Provides `virtual_any`, a wide `any` that combines an `any`, held by value, +with a pointer to the v-table for the contained value - like `virtual_ptr` +combines a pointer to an object with a pointer to its v-table. The v-table +pointer is acquired when the `virtual_any` is created, so methods dispatch on +the contained type without looking it up on every call. Also provides +`make_any_virtual`, which creates a `virtual_any` containing a value of a +statically known type, setting the v-table pointer without any lookup. This +header is included by `std_any.hpp` and `boost_any.hpp`; it can serve any type +with an `any`-like interface, given `virtual_traits` specializations for its +reference types. + [#std_any] ### link:{{BASE_URL}}/include/boost/openmethod/interop/std_any.hpp[] Provides `virtual_traits` specializations that make it possible to use a `std::any` - by const reference, by mutable reference, or by rvalue reference - in virtual parameters. Dispatch is on the type of the contained value. Also provides -`use_std_any_types`, which registers the types that may be contained. +`use_std_any_types`, which registers the types that may be contained; +`virtual_std_any`, an alias for `virtual_any`, and +`make_std_any_virtual`. In addition, the header deletes the +`final_virtual_ptr` overloads for `std::any`, which would otherwise silently +use the v-table of the `any` root class instead of the contained value's. [#boost_any] ### link:{{BASE_URL}}/include/boost/openmethod/interop/boost_any.hpp[] @@ -85,7 +103,11 @@ parameters. Dispatch is on the type of the contained value. Also provides Provides `virtual_traits` specializations that make it possible to use a `boost::any` - by const reference, by mutable reference, or by rvalue reference - in virtual parameters. Dispatch is on the type of the contained value. Also provides -`use_boost_any_types`, which registers the types that may be contained. +`use_boost_any_types`, which registers the types that may be contained; +`virtual_boost_any`, an alias for `virtual_any`, and +`make_boost_any_virtual`. In addition, the header deletes the +`final_virtual_ptr` overloads for `boost::any`, which would otherwise silently +use the v-table of the `any` root class instead of the contained value's. *The headers below are for advanced use*. diff --git a/include/boost/openmethod/interop/boost_any.hpp b/include/boost/openmethod/interop/boost_any.hpp index 6234e5b2..3e77d09e 100644 --- a/include/boost/openmethod/interop/boost_any.hpp +++ b/include/boost/openmethod/interop/boost_any.hpp @@ -8,6 +8,7 @@ #include #include +#include namespace boost::openmethod { @@ -238,6 +239,57 @@ struct use_boost_any_types typename detail::extract_registry::registry, mp11::mp_list>... {}; +//! Alias for a `virtual_any`, in the default registry. +//! +//! With another registry, use `virtual_any` directly. +using virtual_boost_any = virtual_any; + +//! Create a new object and return a `virtual_boost_any` containing it. +//! +//! Create a `Class` from `args`, store it in a `boost::any`, and return a +//! @ref virtual_any with its v-table pointer set to the +//! @ref registry::static_vptr for `Class` - no hash table lookup is +//! involved. +//! +//! @tparam Class The type of the value to create. +//! @tparam Registry A @ref registry. +//! @tparam T Types of the arguments to pass to the constructor of +//! `Class`. +//! @param args Arguments to pass to the constructor of `Class`. +//! @return A `virtual_any` containing a newly created +//! `Class`. +template< + class Class, class Registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY, + typename... T> +inline auto +make_boost_any_virtual(T&&... args) -> virtual_any { + return make_any_virtual( + std::forward(args)...); +} + +// The primary final_virtual_ptr would silently use static_vptr +// - the v-table of the `any` root class, not of the contained value. +// Delete the combination. Both call forms need covering: the non-template +// overloads catch calls that deduce the default registry, and are removed +// from consideration when an explicit template argument list is given, so +// the Registry-only templates - more specialized than the primary - catch +// those. + +template +void final_virtual_ptr(const boost::any&) = delete; +template +void final_virtual_ptr(boost::any&) = delete; +template +void final_virtual_ptr(boost::any&&) = delete; +void final_virtual_ptr(const boost::any&) = delete; +void final_virtual_ptr(boost::any&) = delete; +void final_virtual_ptr(boost::any&&) = delete; + +namespace aliases { +using boost::openmethod::make_boost_any_virtual; +using boost::openmethod::virtual_boost_any; +} // namespace aliases + } // namespace boost::openmethod #endif diff --git a/include/boost/openmethod/interop/std_any.hpp b/include/boost/openmethod/interop/std_any.hpp index 46c340ed..1b0bda96 100644 --- a/include/boost/openmethod/interop/std_any.hpp +++ b/include/boost/openmethod/interop/std_any.hpp @@ -8,6 +8,7 @@ #include #include +#include namespace boost::openmethod { @@ -201,6 +202,57 @@ struct use_std_any_types typename detail::extract_registry::registry, mp11::mp_list>... {}; +//! Alias for a `virtual_any`, in the default registry. +//! +//! With another registry, use `virtual_any` directly. +using virtual_std_any = virtual_any; + +//! Create a new object and return a `virtual_std_any` containing it. +//! +//! Create a `Class` from `args`, store it in a `std::any`, and return a +//! @ref virtual_any with its v-table pointer set to the +//! @ref registry::static_vptr for `Class` - no hash table lookup is +//! involved. +//! +//! @tparam Class The type of the value to create. +//! @tparam Registry A @ref registry. +//! @tparam T Types of the arguments to pass to the constructor of +//! `Class`. +//! @param args Arguments to pass to the constructor of `Class`. +//! @return A `virtual_any` containing a newly created +//! `Class`. +template< + class Class, class Registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY, + typename... T> +inline auto +make_std_any_virtual(T&&... args) -> virtual_any { + return make_any_virtual( + std::forward(args)...); +} + +// The primary final_virtual_ptr would silently use static_vptr +// - the v-table of the `any` root class, not of the contained value. +// Delete the combination. Both call forms need covering: the non-template +// overloads catch calls that deduce the default registry, and are removed +// from consideration when an explicit template argument list is given, so +// the Registry-only templates - more specialized than the primary - catch +// those. + +template +void final_virtual_ptr(const std::any&) = delete; +template +void final_virtual_ptr(std::any&) = delete; +template +void final_virtual_ptr(std::any&&) = delete; +void final_virtual_ptr(const std::any&) = delete; +void final_virtual_ptr(std::any&) = delete; +void final_virtual_ptr(std::any&&) = delete; + +namespace aliases { +using boost::openmethod::make_std_any_virtual; +using boost::openmethod::virtual_std_any; +} // namespace aliases + } // namespace boost::openmethod #endif diff --git a/include/boost/openmethod/interop/virtual_any.hpp b/include/boost/openmethod/interop/virtual_any.hpp new file mode 100644 index 00000000..5afe7036 --- /dev/null +++ b/include/boost/openmethod/interop/virtual_any.hpp @@ -0,0 +1,498 @@ +// Copyright (c) 2018-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) + +#ifndef BOOST_OPENMETHOD_INTEROP_VIRTUAL_ANY_HPP +#define BOOST_OPENMETHOD_INTEROP_VIRTUAL_ANY_HPP + +#include + +#include +#include + +namespace boost::openmethod { + +template +class virtual_any; + +namespace detail { + +template +struct is_virtual_any_aux : std::false_type {}; + +template +struct is_virtual_any_aux> : std::true_type {}; + +} // namespace detail + +//! A wide `any`, combining an `any` and a pointer to a v-table. +//! +//! `virtual_any` is to `any` what @ref virtual_ptr is to a pointer: it +//! carries the v-table pointer for the value stored in the `any`, so +//! methods dispatch on the contained type without looking it up on every +//! call. Unlike `virtual_ptr`, it *owns* its object: the `any` is held by +//! value. +//! +//! The v-table pointer is acquired when the `virtual_any` is created: +//! either from the dynamic type of an existing `any` (a hash table +//! lookup, via `virtual_traits::vptr`), or +//! statically, when the contained type is known at compile time (the +//! value constructor, @ref make_any_virtual, and @ref emplace use @ref +//! registry::static_vptr). +//! +//! Methods take `virtual_any` parameters by reference: `const +//! virtual_any&`, `virtual_any&` or `virtual_any&&`. Overriders receive +//! the *contained* type, by a reference of a compatible category - or the +//! `virtual_any` itself, unchanged, for a catch-all overrider. +//! +//! The contained value cannot be replaced through a `virtual_any` other +//! than via assignment or @ref emplace, which re-derive the v-table +//! pointer, thus maintaining the invariant that the v-table pointer +//! corresponds to the contained type. +//! +//! `Any` can be `std::any`, `boost::any`, or any type that has an +//! `any`-like interface, and specializes `virtual_traits` for its +//! reference types, providing `vptr` and `cast`. +//! +//! @tparam Any An `any` type. +//! @tparam Registry A @ref registry. +template +class virtual_any { + static constexpr bool use_indirect_vptrs = Registry::has_indirect_vptr; + + Any obj; + std::conditional_t vp; + + template + friend struct virtual_traits; + + public: + //! Construct an empty `virtual_any`. + //! + //! The `any` is empty, and the v-table pointer is null. + virtual_any() + : obj(), vp(detail::box_vptr(detail::null_vptr)) { + } + + //! Construct from an `any` (copy). + //! + //! Copies `other`, and acquires the v-table pointer for the contained + //! value, using `virtual_traits::vptr`. + //! + //! @param other An `any`. + virtual_any(const Any& other) + : obj(other), vp(detail::box_vptr( + detail::acquire_vptr(obj))) { + } + + //! Construct from an `any` (move). + //! + //! Moves `other`, and acquires the v-table pointer for the contained + //! value, using `virtual_traits::vptr`. + //! + //! @param other An `any`. + virtual_any(Any&& other) + : obj(std::move(other)), vp(detail::box_vptr( + detail::acquire_vptr(obj))) { + } + + //! Construct from a value. + //! + //! Stores `value` in the `any`, and sets the v-table pointer to the + //! @ref registry::static_vptr for its type - no hash table lookup is + //! involved. The type of `value`, stripped from reference and + //! cv-qualifiers, must be registered in `Registry`. + //! + //! @tparam T The type of the value. + //! @param value The value to store. + template< + typename T, + typename = std::enable_if_t< + !detail::is_virtual_any_aux>::value && + !std::is_same_v, Any> && + std::is_constructible_v>> + virtual_any(T&& value) + : obj(std::forward(value)), + vp(detail::box_vptr( + Registry::template static_vptr>)) { + Registry::require_initialized(); + BOOST_ASSERT(detail::unbox_vptr(vp) != nullptr); + } + + //! Copy constructor. + virtual_any(const virtual_any& other) = default; + + //! Move constructor. + //! + //! Moves the `any`, and sets `other`'s v-table pointer to null. + //! + //! @param other A `virtual_any`. + virtual_any(virtual_any&& other) : obj(std::move(other.obj)), vp(other.vp) { + other.vp = detail::box_vptr(detail::null_vptr); + } + + //! Copy assignment operator. + auto operator=(const virtual_any& other) -> virtual_any& = default; + + //! Move assignment operator. + //! + //! Moves the `any`, and sets `other`'s v-table pointer to null. + //! + //! @param other A `virtual_any`. + auto operator=(virtual_any&& other) -> virtual_any& { + obj = std::move(other.obj); + vp = other.vp; + other.vp = detail::box_vptr(detail::null_vptr); + return *this; + } + + //! Assign from an `any` (copy). + //! + //! Copies `other`, and re-acquires the v-table pointer for the + //! contained value. + //! + //! @param other An `any`. + auto operator=(const Any& other) -> virtual_any& { + obj = other; + vp = detail::box_vptr( + detail::acquire_vptr(obj)); + return *this; + } + + //! Assign from an `any` (move). + //! + //! Moves `other`, and re-acquires the v-table pointer for the + //! contained value. + //! + //! @param other An `any`. + auto operator=(Any&& other) -> virtual_any& { + obj = std::move(other); + vp = detail::box_vptr( + detail::acquire_vptr(obj)); + return *this; + } + + //! Assign from a value. + //! + //! Stores `value` in the `any`, and sets the v-table pointer to the + //! @ref registry::static_vptr for its type - no hash table lookup is + //! involved. + //! + //! @tparam T The type of the value. + //! @param value The value to store. + template< + typename T, + typename = std::enable_if_t< + !detail::is_virtual_any_aux>::value && + !std::is_same_v, Any> && + std::is_constructible_v>> + auto operator=(T&& value) -> virtual_any& { + obj = std::forward(value); + Registry::require_initialized(); + vp = detail::box_vptr( + Registry::template static_vptr>); + BOOST_ASSERT(detail::unbox_vptr(vp) != nullptr); + return *this; + } + + //! Construct a value in place. + //! + //! Stores a `Class` constructed from `args`, and sets the v-table + //! pointer to the @ref registry::static_vptr for `Class` - no hash + //! table lookup is involved. + //! + //! @tparam Class The type of the value to construct. + //! @tparam T Types of the arguments to pass to the constructor. + //! @param args Arguments to pass to the constructor of `Class`. + template + auto emplace(T&&... args) -> void { + obj = Class(std::forward(args)...); + Registry::require_initialized(); + vp = detail::box_vptr( + Registry::template static_vptr); + BOOST_ASSERT(detail::unbox_vptr(vp) != nullptr); + } + + //! Return a reference to the (non-modifiable) `any`. + auto get() const -> const Any& { + return obj; + } + + //! Return the v-table pointer. + auto vptr() const -> vptr_type { + return detail::unbox_vptr(vp); + } + +#ifndef __MRDOCS__ + friend auto + boost_openmethod_vptr(const virtual_any& va, Registry*) -> vptr_type { + return detail::unbox_vptr(va.vp); + } +#endif +}; + +//! Specialize virtual_traits for `const virtual_any&`. +//! +//! Dispatch is on the v-table pointer stored in the `virtual_any`. +//! +//! @tparam Any An `any` type. +//! @tparam Registry A @ref registry. +template +struct virtual_traits&, Registry> { + //! The type used for dispatch. + using virtual_type = Any; + + //! Returns a const reference to the `virtual_any` argument. + //! @param arg A reference to a `virtual_any`. + //! @return A const reference to `arg`. + static auto peek(const virtual_any& arg) + -> const virtual_any& { + return arg; + } + + //! Cast to a type. + //! + //! If `U` is the `virtual_any` itself (by any reference category), + //! returns `arg` unchanged. Otherwise, extracts the stored value + //! using `virtual_traits::cast`. Since the + //! `any` is not modifiable, `U` cannot be a mutable reference. + //! + //! @tparam U The target type (e.g. `const Dog&`, `Dog`). + //! @param arg A reference to a const `virtual_any` method argument. + //! @return The value stored in `arg`, cast to `U`. + template + static auto cast(const virtual_any& arg) -> decltype(auto) { + if constexpr (std::is_same_v< + std::remove_cv_t>, + virtual_any>) { + return (arg); + } else { + return virtual_traits::template cast( + arg.obj); + } + } +}; + +//! Specialize virtual_traits for `virtual_any&` (mutable reference). +//! +//! Dispatch is on the v-table pointer stored in the `virtual_any`. +//! +//! @tparam Any An `any` type. +//! @tparam Registry A @ref registry. +template +struct virtual_traits&, Registry> { + //! The type used for dispatch. + using virtual_type = Any; + + //! Returns a const reference to the `virtual_any` argument. + //! @param arg A reference to a `virtual_any`. + //! @return A const reference to `arg`. + static auto peek(const virtual_any& arg) + -> const virtual_any& { + return arg; + } + + //! Cast to a type. + //! + //! If `U` is the `virtual_any` itself (by mutable reference), returns + //! `arg` unchanged. Otherwise, extracts the stored value using + //! `virtual_traits::cast`. Supports mutable + //! references (e.g. `Dog&`); modifications through the result are + //! visible through the `virtual_any`. + //! + //! @tparam U The target type (e.g. `Dog&`, `const Dog&`, `Dog`). + //! @param arg A mutable reference to the `virtual_any` method + //! argument. + //! @return The value stored in `arg`, cast to `U`. + template + static auto cast(virtual_any& arg) -> decltype(auto) { + if constexpr (std::is_same_v< + std::remove_cv_t>, + virtual_any>) { + return (arg); + } else { + return virtual_traits::template cast(arg.obj); + } + } +}; + +//! Specialize virtual_traits for `virtual_any&&` (xvalue reference). +//! +//! Dispatch is on the v-table pointer stored in the `virtual_any`. +//! +//! @tparam Any An `any` type. +//! @tparam Registry A @ref registry. +template +struct virtual_traits&&, Registry> { + //! The type used for dispatch. + using virtual_type = Any; + + //! Returns a const reference to the `virtual_any` argument. + //! @param arg A reference to a `virtual_any`. + //! @return A const reference to `arg`. + static auto peek(const virtual_any& arg) + -> const virtual_any& { + return arg; + } + + //! Cast to a type. + //! + //! If `U` is the `virtual_any` itself (by rvalue reference), returns + //! `arg` unchanged. Otherwise, extracts the stored value using + //! `virtual_traits::cast`. + //! + //! @tparam U The target type (e.g. `Dog&&`, `const Dog&`, `Dog`). + //! @param arg An rvalue reference to the `virtual_any` method + //! argument. + //! @return The value stored in `arg`, cast to `U`. + template + static auto cast(virtual_any&& arg) -> decltype(auto) { + if constexpr (std::is_same_v< + std::remove_cv_t>, + virtual_any>) { + return std::move(arg); + } else { + return virtual_traits::template cast( + std::move(arg.obj)); + } + } +}; + +namespace detail { + +template +struct is_virtual&> : std::true_type {}; + +template +struct is_virtual&> : std::true_type {}; + +template +struct is_virtual&&> : std::true_type {}; + +template +struct parameter_traits&, Registry> + : virtual_traits&, Registry> {}; + +template +struct parameter_traits&, Registry> + : virtual_traits&, Registry> {}; + +template +struct parameter_traits&&, Registry> + : virtual_traits&&, Registry> {}; + +template +struct validate_method_parameter< + virtual_any, MethodRegistry, void> : std::false_type { + static_assert( + false_t, "virtual_any must be passed by reference"); +}; + +template +struct validate_method_parameter< + virtual_any&, MethodRegistry, void> + : std::bool_constant> { + static_assert( + std::is_same_v, "registry mismatch"); +}; + +template +struct validate_method_parameter< + const virtual_any&, MethodRegistry, void> + : std::bool_constant> { + static_assert( + std::is_same_v, "registry mismatch"); +}; + +template +struct validate_method_parameter< + virtual_any&&, MethodRegistry, void> + : std::bool_constant> { + static_assert( + std::is_same_v, "registry mismatch"); +}; + +// A virtual_any method parameter places no compile-time constraint on the +// corresponding overrider parameter: the adjustment is delegated entirely +// to virtual_traits::cast, like for virtual_ +// parameters. The exact-pair specializations disambiguate with the +// generic specialization in core.hpp, which is neither more nor +// less specialized than . + +template +struct validate_overrider_parameter&, T2, void> + : std::true_type {}; + +template +struct validate_overrider_parameter< + virtual_any&, virtual_any&, void> + : std::true_type {}; + +template +struct validate_overrider_parameter&, T2, void> + : std::true_type {}; + +template +struct validate_overrider_parameter< + const virtual_any&, const virtual_any&, void> + : std::true_type {}; + +template +struct validate_overrider_parameter&&, T2, void> + : std::true_type {}; + +template +struct validate_overrider_parameter< + virtual_any&&, virtual_any&&, void> + : std::true_type {}; + +template +struct select_overrider_virtual_type_aux< + virtual_any&, Q, Registry> { + using type = virtual_type; +}; + +template +struct select_overrider_virtual_type_aux< + const virtual_any&, Q, Registry> { + using type = virtual_type; +}; + +template +struct select_overrider_virtual_type_aux< + virtual_any&&, Q, Registry> { + using type = virtual_type; +}; + +} // namespace detail + +//! Create a new object and return a `virtual_any` containing it. +//! +//! Create a `Class` from `args`, store it in a @ref virtual_any, and set +//! the v-table pointer to the @ref registry::static_vptr for `Class` - no +//! hash table lookup is involved. +//! +//! @tparam Class The type of the value to create. +//! @tparam Any An `any` type. +//! @tparam Registry A @ref registry. +//! @tparam T Types of the arguments to pass to the constructor of +//! `Class`. +//! @param args Arguments to pass to the constructor of `Class`. +//! @return A `virtual_any` containing a newly created +//! `Class`. +template< + class Class, class Any, class Registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY, + typename... T> +inline auto make_any_virtual(T&&... args) -> virtual_any { + return virtual_any(Class(std::forward(args)...)); +} + +namespace aliases { +using boost::openmethod::make_any_virtual; +using boost::openmethod::virtual_any; +} // namespace aliases + +} // namespace boost::openmethod + +#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ed1f9f77..6678cd47 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -163,6 +163,12 @@ openmethod_compile_fail_test( compile_fail_boost_any_const_ref_to_mutable_ref "no matching") openmethod_compile_fail_test( compile_fail_boost_any_mutable_ref_to_rvalue_ref "no matching") +openmethod_compile_fail_test( + compile_fail_virtual_any_by_value "virtual_any must be passed by reference") +# "use of a deleted function" on gcc, "call to deleted function" on clang, +# "attempting to reference a deleted function" on MSVC. +openmethod_compile_fail_test( + compile_fail_final_virtual_ptr_std_any "deleted function") if (TARGET Boost::dll) add_subdirectory(dynamic_loading) diff --git a/test/compile_fail_final_virtual_ptr_std_any.cpp b/test/compile_fail_final_virtual_ptr_std_any.cpp new file mode 100644 index 00000000..60910da3 --- /dev/null +++ b/test/compile_fail_final_virtual_ptr_std_any.cpp @@ -0,0 +1,27 @@ +// Copyright (c) 2018-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +#include + +#include +#include + +using namespace boost::openmethod; + +struct Dog { + std::string name; +}; + +BOOST_OPENMETHOD_REGISTER(use_std_any_types); + +int main() { + // The primary final_virtual_ptr would use static_vptr - the + // v-table of the `any` root class, not of the contained value. The + // combination is deleted; use virtual_any instead. + std::any spot(Dog{"Spot"}); + final_virtual_ptr(spot); + return 0; +} diff --git a/test/compile_fail_virtual_any_by_value.cpp b/test/compile_fail_virtual_any_by_value.cpp new file mode 100644 index 00000000..97ea54ba --- /dev/null +++ b/test/compile_fail_virtual_any_by_value.cpp @@ -0,0 +1,26 @@ +// Copyright (c) 2018-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +#include + +#include +#include + +using namespace boost::openmethod; + +struct Dog { + std::string name; +}; + +BOOST_OPENMETHOD_REGISTER(use_std_any_types); + +// A virtual_any method parameter must be a reference: passing it by value +// would copy the `any` - and its payload - on every call. +BOOST_OPENMETHOD(name, (virtual_std_any), std::string); + +int main() { + return 0; +} diff --git a/test/test_virtual_any_boost.cpp b/test/test_virtual_any_boost.cpp new file mode 100644 index 00000000..a77b2e37 --- /dev/null +++ b/test/test_virtual_any_boost.cpp @@ -0,0 +1,244 @@ +// Copyright (c) 2018-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +#include +#include + +#include +#include +#include + +#define BOOST_TEST_MODULE openmethod +#include + +using namespace boost::openmethod; + +#define MAKE_CLASSES() \ + struct Dog { \ + std::string name; \ + }; \ + \ + use_boost_any_types BOOST_OPENMETHOD_GENSYM; + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as const virtual_boost_any& (const ref) + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (const virtual_boost_any&), std::string); + +// The overriders can use the macro: the value constructor of virtual_any +// makes the overrider's parameter convertible to the method's, so the +// method is located. + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { + return name; +} + +// A catch-all overrider may keep the wrapper. +BOOST_OPENMETHOD_OVERRIDE(name, (const virtual_boost_any& va), std::string) { + return !va.get().empty() ? "something" : "nothing"; +} + +BOOST_AUTO_TEST_CASE(virtual_any_by_const_ref) { + initialize(trace()); + + // from an `any`: the v-table pointer is looked up from the dynamic + // type of the contained value + const boost::any spot_any(Dog{"Spot"}); + virtual_boost_any spot = spot_any; + BOOST_TEST(spot.vptr() == default_registry::static_vptr); + BOOST_TEST(name(spot) == "Spot the dog"); + + // from a value: the v-table pointer is set statically + virtual_boost_any rex = Dog{"Rex"}; + BOOST_TEST(rex.vptr() == default_registry::static_vptr); + BOOST_TEST(name(rex) == "Rex the dog"); + + auto felix = make_boost_any_virtual("Felix the cat"); + BOOST_TEST(felix.vptr() == default_registry::static_vptr); + BOOST_TEST(name(felix) == "Felix the cat"); + + // a value converts to a (temporary) virtual_any at the call site + BOOST_TEST(name(Dog{"Fido"}) == "Fido the dog"); + + // `int` is registered, but has no specific overrider: the catch-all, + // registered for the `boost::any` root, applies + BOOST_TEST(name(42) == "something"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as virtual_boost_any& (mutable ref) + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(bump, (virtual_boost_any&), std::string); + +// BOOST_OPENMETHOD_OVERRIDE cannot express this: a temporary virtual_any +// binds to `const virtual_boost_any&` and to `virtual_boost_any&&`, but +// nothing binds to a mutable lvalue reference. Register directly via +// method<...>::override instead - the primitive the macro itself +// expands to. + +using bump_method = + BOOST_OPENMETHOD_TYPE(bump, (virtual_boost_any&), std::string); + +auto bump_dog(Dog& dog) -> std::string { + dog.name += " Jr."; + return dog.name + " the dog"; +} + +auto bump_int(int& value) -> std::string { + ++value; + return "bumped"; +} + +BOOST_OPENMETHOD_REGISTER(bump_method::override); +BOOST_OPENMETHOD_REGISTER(bump_method::override); + +BOOST_AUTO_TEST_CASE(virtual_any_by_mutable_ref) { + initialize(trace()); + + virtual_boost_any spot = Dog{"Spot"}; + BOOST_TEST(bump(spot) == "Spot Jr. the dog"); + // the mutation is visible through the virtual_any + BOOST_TEST(boost::any_cast(spot.get()).name == "Spot Jr."); + + virtual_boost_any answer = 41; + BOOST_TEST(bump(answer) == "bumped"); + BOOST_TEST(boost::any_cast(answer.get()) == 42); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as virtual_boost_any&& (xvalue ref) + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(steal, (virtual_boost_any&&), std::string); + +BOOST_OPENMETHOD_OVERRIDE(steal, (Dog && dog), std::string) { + Dog stolen(std::move(dog)); + return stolen.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(steal, (std::string && name), std::string) { + std::string stolen(std::move(name)); + return stolen; +} + +BOOST_AUTO_TEST_CASE(virtual_any_by_xvalue_ref) { + initialize(trace()); + + virtual_boost_any spot = Dog{"Spot"}; + BOOST_TEST(steal(std::move(spot)) == "Spot the dog"); + // the overrider moved the name out; the virtual_any still owns the Dog + BOOST_TEST(!spot.get().empty()); + BOOST_TEST(boost::any_cast(spot.get()).name == ""); + + BOOST_TEST( + steal(make_boost_any_virtual("Felix the cat")) == + "Felix the cat"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// value semantics + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (const virtual_boost_any&), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_AUTO_TEST_CASE(virtual_any_value_semantics) { + initialize(trace()); + + virtual_boost_any empty; + BOOST_TEST(empty.get().empty()); + BOOST_TEST(empty.vptr() == nullptr); + + virtual_boost_any rex = Dog{"Rex"}; + + // copy: independent payloads, same v-table pointer + auto copy = rex; + BOOST_TEST(copy.vptr() == rex.vptr()); + BOOST_TEST(name(copy) == "Rex the dog"); + BOOST_TEST(name(rex) == "Rex the dog"); // original unaffected + + // move: the source's v-table pointer is nulled + auto moved = std::move(copy); + BOOST_TEST(moved.vptr() == default_registry::static_vptr); + BOOST_TEST(copy.vptr() == nullptr); + BOOST_TEST(name(moved) == "Rex the dog"); + + // assignment from an `any` re-derives the v-table pointer + boost::any felix_any(std::string{"Felix"}); + moved = felix_any; + BOOST_TEST(moved.vptr() == default_registry::static_vptr); + + // assignment from a value sets it statically + moved = Dog{"Snoopy"}; + BOOST_TEST(moved.vptr() == default_registry::static_vptr); + BOOST_TEST(name(moved) == "Snoopy the dog"); + + // emplace constructs in place and sets it statically + moved.emplace("Sylvester"); + BOOST_TEST(moved.vptr() == default_registry::static_vptr); + BOOST_TEST(boost::any_cast(moved.get()) == "Sylvester"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// indirect vptrs + +struct Dog { + std::string name; +}; + +use_boost_any_types + BOOST_OPENMETHOD_GENSYM; + +using name_method = method< + struct name_id, + std::string(const virtual_any&), + indirect_registry>; + +auto name_dog(const Dog& dog) -> std::string { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_REGISTER(name_method::override); + +BOOST_AUTO_TEST_CASE(virtual_any_indirect_vptr) { + initialize(); + + boost::any spot_any(Dog{"Spot"}); + virtual_any spot = spot_any; + BOOST_TEST(spot.vptr() == indirect_registry::static_vptr); + BOOST_TEST(name_method::fn(spot) == "Spot the dog"); + + virtual_any rex = Dog{"Rex"}; + BOOST_TEST(name_method::fn(rex) == "Rex the dog"); +} +} // namespace BOOST_OPENMETHOD_GENSYM diff --git a/test/test_virtual_any_std.cpp b/test/test_virtual_any_std.cpp new file mode 100644 index 00000000..9cefe118 --- /dev/null +++ b/test/test_virtual_any_std.cpp @@ -0,0 +1,244 @@ +// Copyright (c) 2018-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +#include +#include + +#include +#include +#include + +#define BOOST_TEST_MODULE openmethod +#include + +using namespace boost::openmethod; + +#define MAKE_CLASSES() \ + struct Dog { \ + std::string name; \ + }; \ + \ + use_std_any_types BOOST_OPENMETHOD_GENSYM; + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as const virtual_std_any& (const ref) + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (const virtual_std_any&), std::string); + +// The overriders can use the macro: the value constructor of virtual_any +// makes the overrider's parameter convertible to the method's, so the +// method is located. + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { + return name; +} + +// A catch-all overrider may keep the wrapper. +BOOST_OPENMETHOD_OVERRIDE(name, (const virtual_std_any& va), std::string) { + return va.get().has_value() ? "something" : "nothing"; +} + +BOOST_AUTO_TEST_CASE(virtual_any_by_const_ref) { + initialize(trace()); + + // from an `any`: the v-table pointer is looked up from the dynamic + // type of the contained value + const std::any spot_any(Dog{"Spot"}); + virtual_std_any spot = spot_any; + BOOST_TEST(spot.vptr() == default_registry::static_vptr); + BOOST_TEST(name(spot) == "Spot the dog"); + + // from a value: the v-table pointer is set statically + virtual_std_any rex = Dog{"Rex"}; + BOOST_TEST(rex.vptr() == default_registry::static_vptr); + BOOST_TEST(name(rex) == "Rex the dog"); + + auto felix = make_std_any_virtual("Felix the cat"); + BOOST_TEST(felix.vptr() == default_registry::static_vptr); + BOOST_TEST(name(felix) == "Felix the cat"); + + // a value converts to a (temporary) virtual_any at the call site + BOOST_TEST(name(Dog{"Fido"}) == "Fido the dog"); + + // `int` is registered, but has no specific overrider: the catch-all, + // registered for the `std::any` root, applies + BOOST_TEST(name(42) == "something"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as virtual_std_any& (mutable ref) + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(bump, (virtual_std_any&), std::string); + +// BOOST_OPENMETHOD_OVERRIDE cannot express this: a temporary virtual_any +// binds to `const virtual_std_any&` and to `virtual_std_any&&`, but +// nothing binds to a mutable lvalue reference. Register directly via +// method<...>::override instead - the primitive the macro itself +// expands to. + +using bump_method = + BOOST_OPENMETHOD_TYPE(bump, (virtual_std_any&), std::string); + +auto bump_dog(Dog& dog) -> std::string { + dog.name += " Jr."; + return dog.name + " the dog"; +} + +auto bump_int(int& value) -> std::string { + ++value; + return "bumped"; +} + +BOOST_OPENMETHOD_REGISTER(bump_method::override); +BOOST_OPENMETHOD_REGISTER(bump_method::override); + +BOOST_AUTO_TEST_CASE(virtual_any_by_mutable_ref) { + initialize(trace()); + + virtual_std_any spot = Dog{"Spot"}; + BOOST_TEST(bump(spot) == "Spot Jr. the dog"); + // the mutation is visible through the virtual_any + BOOST_TEST(std::any_cast(spot.get()).name == "Spot Jr."); + + virtual_std_any answer = 41; + BOOST_TEST(bump(answer) == "bumped"); + BOOST_TEST(std::any_cast(answer.get()) == 42); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as virtual_std_any&& (xvalue ref) + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(steal, (virtual_std_any&&), std::string); + +BOOST_OPENMETHOD_OVERRIDE(steal, (Dog && dog), std::string) { + Dog stolen(std::move(dog)); + return stolen.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(steal, (std::string && name), std::string) { + std::string stolen(std::move(name)); + return stolen; +} + +BOOST_AUTO_TEST_CASE(virtual_any_by_xvalue_ref) { + initialize(trace()); + + virtual_std_any spot = Dog{"Spot"}; + BOOST_TEST(steal(std::move(spot)) == "Spot the dog"); + // the overrider moved the name out; the virtual_any still owns the Dog + BOOST_TEST(spot.get().has_value()); + BOOST_TEST(std::any_cast(spot.get()).name == ""); + + BOOST_TEST( + steal(make_std_any_virtual("Felix the cat")) == + "Felix the cat"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// value semantics + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (const virtual_std_any&), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_AUTO_TEST_CASE(virtual_any_value_semantics) { + initialize(trace()); + + virtual_std_any empty; + BOOST_TEST(!empty.get().has_value()); + BOOST_TEST(empty.vptr() == nullptr); + + virtual_std_any rex = Dog{"Rex"}; + + // copy: independent payloads, same v-table pointer + auto copy = rex; + BOOST_TEST(copy.vptr() == rex.vptr()); + BOOST_TEST(name(copy) == "Rex the dog"); + BOOST_TEST(name(rex) == "Rex the dog"); // original unaffected + + // move: the source's v-table pointer is nulled + auto moved = std::move(copy); + BOOST_TEST(moved.vptr() == default_registry::static_vptr); + BOOST_TEST(copy.vptr() == nullptr); + BOOST_TEST(name(moved) == "Rex the dog"); + + // assignment from an `any` re-derives the v-table pointer + std::any felix_any(std::string{"Felix"}); + moved = felix_any; + BOOST_TEST(moved.vptr() == default_registry::static_vptr); + + // assignment from a value sets it statically + moved = Dog{"Snoopy"}; + BOOST_TEST(moved.vptr() == default_registry::static_vptr); + BOOST_TEST(name(moved) == "Snoopy the dog"); + + // emplace constructs in place and sets it statically + moved.emplace("Sylvester"); + BOOST_TEST(moved.vptr() == default_registry::static_vptr); + BOOST_TEST(std::any_cast(moved.get()) == "Sylvester"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// indirect vptrs + +struct Dog { + std::string name; +}; + +use_std_any_types + BOOST_OPENMETHOD_GENSYM; + +using name_method = method< + struct name_id, + std::string(const virtual_any&), + indirect_registry>; + +auto name_dog(const Dog& dog) -> std::string { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_REGISTER(name_method::override); + +BOOST_AUTO_TEST_CASE(virtual_any_indirect_vptr) { + initialize(); + + std::any spot_any(Dog{"Spot"}); + virtual_any spot = spot_any; + BOOST_TEST(spot.vptr() == indirect_registry::static_vptr); + BOOST_TEST(name_method::fn(spot) == "Spot the dog"); + + virtual_any rex = Dog{"Rex"}; + BOOST_TEST(name_method::fn(rex) == "Rex the dog"); +} +} // namespace BOOST_OPENMETHOD_GENSYM From 475b05b8bf64bef3e27545500160a777cc995eff Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 8 Aug 2026 12:22:50 -0400 Subject: [PATCH 10/64] probe virtual_traits::vptr with the argument type, not type_id acquire_vptr and method::vptr detected a traits-supplied vptr with has_vptr, type_id>, i.e. by asking whether traits::vptr is callable with a type_id (a const void*). The member takes a reference to the any, so the probe only passed because std::any and boost::any happen to have a greedy converting constructor that accepts a const void*. An any-like type without such a constructor would silently fail the probe and fall through to the vptr policy's dynamic_vptr, which keys the lookup on typeid(wrapper) - the wrapper class itself, not the contained value. Probe with the actual argument type instead, making the detection ask the intended question: does this specialization provide a vptr member. Co-Authored-By: Claude Fable 5 --- include/boost/openmethod/core.hpp | 4 ++-- test/test_dispatch_boost_any.cpp | 9 ++++++--- test/test_dispatch_std_any.cpp | 9 +++++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index 180adc77..429f8d02 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -536,7 +536,7 @@ decltype(auto) acquire_vptr(const ArgType& arg) { return boost_openmethod_vptr(arg, static_cast(nullptr)); } else if constexpr (has_vptr< virtual_traits, - type_id>) { + const ArgType&>) { return virtual_traits::vptr(arg); } else { return Registry::template policy::dynamic_vptr(arg); @@ -2545,7 +2545,7 @@ BOOST_FORCEINLINE auto method::vptr( return boost_openmethod_vptr(obj, static_cast(nullptr)); } else if constexpr (detail::has_vptr< virtual_traits, - type_id>) { + decltype(obj)>) { return virtual_traits::vptr(obj); } else { return Registry::template policy::dynamic_vptr(obj); diff --git a/test/test_dispatch_boost_any.cpp b/test/test_dispatch_boost_any.cpp index bfd850db..fa2317d3 100644 --- a/test/test_dispatch_boost_any.cpp +++ b/test/test_dispatch_boost_any.cpp @@ -28,7 +28,8 @@ namespace BOOST_OPENMETHOD_GENSYM { // pass virtual args as const boost::any& (const ref) static_assert(detail::has_vptr< - virtual_traits, type_id>); + virtual_traits, + const boost::any&>); MAKE_CLASSES(); @@ -67,7 +68,8 @@ namespace BOOST_OPENMETHOD_GENSYM { // pass virtual args as boost::any& (mutable ref) static_assert( - detail::has_vptr, type_id>); + detail::has_vptr< + virtual_traits, const boost::any&>); MAKE_CLASSES(); @@ -130,7 +132,8 @@ namespace BOOST_OPENMETHOD_GENSYM { // pass virtual args as boost::any&& (xvalue ref) static_assert( - detail::has_vptr, type_id>); + detail::has_vptr< + virtual_traits, const boost::any&>); MAKE_CLASSES(); diff --git a/test/test_dispatch_std_any.cpp b/test/test_dispatch_std_any.cpp index 9c8e7025..88626296 100644 --- a/test/test_dispatch_std_any.cpp +++ b/test/test_dispatch_std_any.cpp @@ -27,8 +27,9 @@ namespace BOOST_OPENMETHOD_GENSYM { // ----------------------------------------------------------------------------- // pass virtual args as const std::any& (const ref) -static_assert(detail::has_vptr< - virtual_traits, type_id>); +static_assert( + detail::has_vptr< + virtual_traits, const std::any&>); MAKE_CLASSES(); @@ -67,7 +68,7 @@ namespace BOOST_OPENMETHOD_GENSYM { // pass virtual args as std::any& (mutable ref) static_assert(detail::has_vptr< - virtual_traits, type_id>); + virtual_traits, const std::any&>); MAKE_CLASSES(); @@ -130,7 +131,7 @@ namespace BOOST_OPENMETHOD_GENSYM { // pass virtual args as std::any&& (xvalue ref) static_assert(detail::has_vptr< - virtual_traits, type_id>); + virtual_traits, const std::any&>); MAKE_CLASSES(); From a117a28db9637eea06c6a2e98ab507b0fc015856 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 8 Aug 2026 12:33:13 -0400 Subject: [PATCH 11/64] any traits: pass the any through to catch-all overriders An overrider may take the method's `any` parameter itself, acting as a catch-all for contained types that have no more specific overrider. The virtual_traits cast members passed U to any_cast unconditionally, and any_cast to the any's own type throws unless the any contains an any. Return the argument unchanged when U is the any, by value or by any reference category - as virtual_any's traits already did. Co-Authored-By: Claude Fable 5 --- .../boost/openmethod/interop/boost_any.hpp | 24 ++++++++++-- include/boost/openmethod/interop/std_any.hpp | 38 +++++++++++++++---- test/test_dispatch_boost_any.cpp | 13 +++++++ test/test_dispatch_std_any.cpp | 13 +++++++ 4 files changed, 78 insertions(+), 10 deletions(-) diff --git a/include/boost/openmethod/interop/boost_any.hpp b/include/boost/openmethod/interop/boost_any.hpp index 3e77d09e..850f9d34 100644 --- a/include/boost/openmethod/interop/boost_any.hpp +++ b/include/boost/openmethod/interop/boost_any.hpp @@ -87,7 +87,13 @@ struct virtual_traits { !std::is_reference_v || std::is_const_v>>> static auto cast(const boost::any& arg) -> decltype(auto) { - return boost::any_cast(arg); + if constexpr (std::is_same_v< + std::remove_cv_t>, + boost::any>) { + return (arg); + } else { + return boost::any_cast(arg); + } } }; @@ -150,7 +156,13 @@ struct virtual_traits { template< typename U, typename = std::enable_if_t>> static auto cast(boost::any& arg) -> decltype(auto) { - return boost::any_cast(arg); + if constexpr (std::is_same_v< + std::remove_cv_t>, + boost::any>) { + return (arg); + } else { + return boost::any_cast(arg); + } } }; @@ -213,7 +225,13 @@ struct virtual_traits { !std::is_lvalue_reference_v || std::is_const_v>>> static auto cast(boost::any&& arg) -> decltype(auto) { - return boost::any_cast(std::move(arg)); + if constexpr (std::is_same_v< + std::remove_cv_t>, + boost::any>) { + return std::move(arg); + } else { + return boost::any_cast(std::move(arg)); + } } }; diff --git a/include/boost/openmethod/interop/std_any.hpp b/include/boost/openmethod/interop/std_any.hpp index 1b0bda96..924a0d01 100644 --- a/include/boost/openmethod/interop/std_any.hpp +++ b/include/boost/openmethod/interop/std_any.hpp @@ -68,7 +68,9 @@ struct virtual_traits { //! Cast to a type. //! - //! Extracts the stored value using `std::any_cast`. Since the `any` + //! If `U` is `std::any` itself (by value or const reference), returns + //! `arg` unchanged - the catch-all overrider case. Otherwise, + //! extracts the stored value using `std::any_cast`. Since the `any` //! argument is const, `U` cannot be a mutable reference. //! //! @tparam U The target type (e.g. `const Dog&`, `Dog`). @@ -76,7 +78,13 @@ struct virtual_traits { //! @return The value stored in `arg`, cast to `U`. template static auto cast(const std::any& arg) -> decltype(auto) { - return std::any_cast(arg); + if constexpr (std::is_same_v< + std::remove_cv_t>, + std::any>) { + return (arg); + } else { + return std::any_cast(arg); + } } }; @@ -120,7 +128,9 @@ struct virtual_traits { //! Cast to a type. //! - //! Extracts the stored value using `std::any_cast`. Supports mutable + //! If `U` is `std::any` itself (by reference or by value), returns + //! `arg` unchanged - the catch-all overrider case. Otherwise, + //! extracts the stored value using `std::any_cast`. Supports mutable //! references (e.g. `Dog&`) because the `any` argument is not const; //! modifications through the result are visible through the `any`. //! @@ -129,7 +139,13 @@ struct virtual_traits { //! @return The value stored in `arg`, cast to `U`. template static auto cast(std::any& arg) -> decltype(auto) { - return std::any_cast(arg); + if constexpr (std::is_same_v< + std::remove_cv_t>, + std::any>) { + return (arg); + } else { + return std::any_cast(arg); + } } }; @@ -173,14 +189,22 @@ struct virtual_traits { //! Cast to a type. //! - //! Extracts the stored value using `std::any_cast`. + //! If `U` is `std::any` itself, returns `arg` unchanged - the + //! catch-all overrider case. Otherwise, extracts the stored value + //! using `std::any_cast`. //! - //! @tparam U The target type (e.g. `Dog&`, `const Dog&`, `Dog`). + //! @tparam U The target type (e.g. `Dog&&`, `const Dog&`, `Dog`). //! @param arg An rvalue reference to the `std::any` method argument. //! @return The value stored in `arg`, cast to `U`. template static auto cast(std::any&& arg) -> decltype(auto) { - return std::any_cast(std::move(arg)); + if constexpr (std::is_same_v< + std::remove_cv_t>, + std::any>) { + return std::move(arg); + } else { + return std::any_cast(std::move(arg)); + } } }; diff --git a/test/test_dispatch_boost_any.cpp b/test/test_dispatch_boost_any.cpp index fa2317d3..7b7b1ebb 100644 --- a/test/test_dispatch_boost_any.cpp +++ b/test/test_dispatch_boost_any.cpp @@ -49,6 +49,15 @@ BOOST_OPENMETHOD_OVERRIDE(name, (const int& value), std::string) { return os.str(); } +// A catch-all overrider may take the `any` itself; the argument is passed +// through unchanged, instead of going through boost::any_cast, which +// would throw unless the `any` contains an `any`. +use_boost_any_types BOOST_OPENMETHOD_GENSYM; + +BOOST_OPENMETHOD_OVERRIDE(name, (const boost::any& arg), std::string) { + return !arg.empty() ? "something" : "nothing"; +} + BOOST_AUTO_TEST_CASE(boost_any_by_const_ref) { initialize(trace()); @@ -59,6 +68,10 @@ BOOST_AUTO_TEST_CASE(boost_any_by_const_ref) { BOOST_TEST(name(spot) == "Spot the dog"); BOOST_TEST(name(felix) == "Felix the cat"); BOOST_TEST(name(answer) == "42 the integer"); + + // `double` is registered but has no specific overrider: the catch-all, + // registered for the `boost::any` root, applies + BOOST_TEST(name(boost::any(1.5)) == "something"); } } // namespace BOOST_OPENMETHOD_GENSYM diff --git a/test/test_dispatch_std_any.cpp b/test/test_dispatch_std_any.cpp index 88626296..7cd2a451 100644 --- a/test/test_dispatch_std_any.cpp +++ b/test/test_dispatch_std_any.cpp @@ -49,6 +49,15 @@ BOOST_OPENMETHOD_OVERRIDE(name, (const int& value), std::string) { return os.str(); } +// A catch-all overrider may take the `any` itself; the argument is passed +// through unchanged, instead of going through std::any_cast, which would +// throw unless the `any` contains an `any`. +use_std_any_types BOOST_OPENMETHOD_GENSYM; + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::any& arg), std::string) { + return arg.has_value() ? "something" : "nothing"; +} + BOOST_AUTO_TEST_CASE(std_any_by_const_ref) { initialize(trace()); @@ -59,6 +68,10 @@ BOOST_AUTO_TEST_CASE(std_any_by_const_ref) { BOOST_TEST(name(spot) == "Spot the dog"); BOOST_TEST(name(felix) == "Felix the cat"); BOOST_TEST(name(answer) == "42 the integer"); + + // `double` is registered but has no specific overrider: the catch-all, + // registered for the `std::any` root, applies + BOOST_TEST(name(std::any(1.5)) == "something"); } } // namespace BOOST_OPENMETHOD_GENSYM From a6d56d2347aa6963e73d09b401602e1bc74021a4 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 8 Aug 2026 12:33:27 -0400 Subject: [PATCH 12/64] use_*_any_types: do not register a trailing registry as a class The registrars expanded their whole template parameter pack into use_class_aux instantiations, so a trailing registry argument - accepted, and used to select the registry - was also registered as a class derived from the any root. Harmless, but wrong. Factor the expansion into detail::use_any_types_aux (in virtual_any.hpp, shared by all the any interop headers), driven by extract_registry's `others` list, which excludes the registry. Co-Authored-By: Claude Fable 5 --- include/boost/openmethod/interop/boost_any.hpp | 9 +++------ include/boost/openmethod/interop/std_any.hpp | 9 +++------ include/boost/openmethod/interop/virtual_any.hpp | 11 +++++++++++ 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/include/boost/openmethod/interop/boost_any.hpp b/include/boost/openmethod/interop/boost_any.hpp index 850f9d34..3ea1982a 100644 --- a/include/boost/openmethod/interop/boost_any.hpp +++ b/include/boost/openmethod/interop/boost_any.hpp @@ -250,12 +250,9 @@ struct virtual_traits { //! followed by a @ref registry. template struct use_boost_any_types - : detail::use_class_aux< - typename detail::extract_registry::registry, - mp11::mp_list>, - detail::use_class_aux< - typename detail::extract_registry::registry, - mp11::mp_list>... {}; + : detail::use_any_types_aux< + typename detail::extract_registry::registry, boost::any, + typename detail::extract_registry::others> {}; //! Alias for a `virtual_any`, in the default registry. //! diff --git a/include/boost/openmethod/interop/std_any.hpp b/include/boost/openmethod/interop/std_any.hpp index 924a0d01..5fff708d 100644 --- a/include/boost/openmethod/interop/std_any.hpp +++ b/include/boost/openmethod/interop/std_any.hpp @@ -219,12 +219,9 @@ struct virtual_traits { //! followed by a @ref registry. template struct use_std_any_types - : detail::use_class_aux< - typename detail::extract_registry::registry, - mp11::mp_list>, - detail::use_class_aux< - typename detail::extract_registry::registry, - mp11::mp_list>... {}; + : detail::use_any_types_aux< + typename detail::extract_registry::registry, std::any, + typename detail::extract_registry::others> {}; //! Alias for a `virtual_any`, in the default registry. //! diff --git a/include/boost/openmethod/interop/virtual_any.hpp b/include/boost/openmethod/interop/virtual_any.hpp index 5afe7036..bb925538 100644 --- a/include/boost/openmethod/interop/virtual_any.hpp +++ b/include/boost/openmethod/interop/virtual_any.hpp @@ -24,6 +24,17 @@ struct is_virtual_any_aux : std::false_type {}; template struct is_virtual_any_aux> : std::true_type {}; +// Common implementation for the use_*_any_types registrars: register Root +// as a class, and each element of the Classes list as a class derived +// from Root. +template +struct use_any_types_aux; + +template +struct use_any_types_aux> + : use_class_aux>, + use_class_aux>... {}; + } // namespace detail //! A wide `any`, combining an `any` and a pointer to a v-table. From 495abc268e8b12ab5f7b833220a3eea7a591e520 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 8 Aug 2026 12:35:27 -0400 Subject: [PATCH 13/64] support boost::type_erasure (#21) Add interop/boost_type_erasure.hpp: dispatch on the type bound to a boost::type_erasure::any, via virtual_traits and the vptr policies' type-id-keyed entry point - the same approach as the std::any and boost::any interop, with no custom rtti policy or registry. Dispatch keys on the std::type_info returned by typeid_of, so the only requirement on the user's Concept is typeid_<>, which `relaxed` already implies. virtual_traits specializations, generic over the Concept, cover the owning flavor by const, mutable and rvalue reference, and the reference-wrapper flavors (any, any) by value - they are cheap, two-word handles, and te's idiomatic parameter carriers. All use the owning flavor as their virtual_type, so a single registered root per Concept serves every parameter form; overriders receive the bound type by a reference of a compatible category, or the any itself as a catch-all. type_erasure's any_cast has no rvalue overload, so the xvalue trait moves the result of a mutable-reference cast - for the owning flavor only, since the rvalue-ness of a reference wrapper says nothing about the referent's ownership. Casts that cannot work (mutable access to const-bound values, moving out of borrowed referents) are removed from the overload set, mirroring the boost::any constraints. use_type_erasure_types registers the bound types under the Concept's root, normalizing Any to the owning flavor. virtual_any composes with no extra code: virtual_any> looks the v-table pointer up once, at construction - recovering O(1) vptr acquisition, which the concept-interface-injection approach sketched in #21 obtained at the cost of naming the policy inside the user's Concept. The final_virtual_ptr overloads for type_erasure::any are deleted: the primary would silently use the root's static v-table pointer. Dispatch on the reference flavors is on the type bound at construction, never the C++ RTTI dynamic type of the referent; an empty relaxed any yields typeid(void), reported as missing_class under runtime checks. Co-Authored-By: Claude Fable 5 --- CMakeLists.txt | 1 + doc/modules/ROOT/pages/ref_headers.adoc | 16 + .../openmethod/interop/boost_type_erasure.hpp | 490 ++++++++++++++++++ test/CMakeLists.txt | 7 + test/Jamfile | 1 + ...le_fail_final_virtual_ptr_type_erasure.cpp | 33 ++ test/compile_fail_type_erasure_by_value.cpp | 34 ++ ..._type_erasure_const_ref_to_mutable_ref.cpp | 37 ++ test/test_dispatch_type_erasure.cpp | 291 +++++++++++ 9 files changed, 910 insertions(+) create mode 100644 include/boost/openmethod/interop/boost_type_erasure.hpp create mode 100644 test/compile_fail_final_virtual_ptr_type_erasure.cpp create mode 100644 test/compile_fail_type_erasure_by_value.cpp create mode 100644 test/compile_fail_type_erasure_const_ref_to_mutable_ref.cpp create mode 100644 test/test_dispatch_type_erasure.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 193b3939..9d09f7c2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -95,6 +95,7 @@ set( if (BOOST_OPENMETHOD_BUILD_TESTS OR BOOST_OPENMETHOD_MRDOCS_BUILD) list(APPEND BOOST_OPENMETHOD_DEPENDENCIES Boost::smart_ptr) list(APPEND BOOST_OPENMETHOD_DEPENDENCIES Boost::any) + list(APPEND BOOST_OPENMETHOD_DEPENDENCIES Boost::type_erasure) endif() foreach (BOOST_OPENMETHOD_DEPENDENCY ${BOOST_OPENMETHOD_DEPENDENCIES}) diff --git a/doc/modules/ROOT/pages/ref_headers.adoc b/doc/modules/ROOT/pages/ref_headers.adoc index b8c89482..03613d1f 100644 --- a/doc/modules/ROOT/pages/ref_headers.adoc +++ b/doc/modules/ROOT/pages/ref_headers.adoc @@ -109,6 +109,22 @@ parameters. Dispatch is on the type of the contained value. Also provides `final_virtual_ptr` overloads for `boost::any`, which would otherwise silently use the v-table of the `any` root class instead of the contained value's. +[#boost_type_erasure] +### link:{{BASE_URL}}/include/boost/openmethod/interop/boost_type_erasure.hpp[] + +Provides `virtual_traits` specializations that make it possible to use a +`boost::type_erasure::any` in virtual parameters: the owning flavor by const, +mutable or rvalue reference, and the reference-wrapper flavors +(`any`, `any`) by value. Dispatch is on +the type of the bound value, obtained via `boost::type_erasure::typeid_of`; the +Concept must contain `boost::type_erasure::typeid_<>`, which `relaxed` implies. +Also provides `use_type_erasure_types`, which registers the types that may be +bound; `virtual_any>` works as well, and looks the v-table pointer +up only once, at construction. In addition, the header deletes the +`final_virtual_ptr` overloads for `boost::type_erasure::any`, which would +otherwise silently use the v-table of the `any` root class instead of the bound +value's. + *The headers below are for advanced use*. ## Pre-Core Headers diff --git a/include/boost/openmethod/interop/boost_type_erasure.hpp b/include/boost/openmethod/interop/boost_type_erasure.hpp new file mode 100644 index 00000000..ae0461a2 --- /dev/null +++ b/include/boost/openmethod/interop/boost_type_erasure.hpp @@ -0,0 +1,490 @@ +// Copyright (c) 2018-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) + +#ifndef BOOST_OPENMETHOD_INTEROP_BOOST_TYPE_ERASURE_HPP +#define BOOST_OPENMETHOD_INTEROP_BOOST_TYPE_ERASURE_HPP + +#include +#include +#include +#include + +#include +#include + +#include +#include + +// Dispatch on the type contained in a boost::type_erasure::any. +// +// The Concept must contain boost::type_erasure::typeid_<> - which +// `relaxed` already implies - so that `typeid_of` can identify the +// contained value. Dispatch is on the `std::type_info` object returned by +// `typeid_of`: the type of the contained value for the owning flavor +// (`any`), or the type *bound at construction* for the reference +// flavors (`any`, `any`) - never +// the C++ RTTI dynamic type of the referent. +// +// Supported virtual parameter forms: +// - `virtual_&>`, `virtual_&>`, +// `virtual_&&>` - the owning flavor, by reference, like +// `std::any`; +// - `virtual_>` and +// `virtual_>` - the reference-wrapper +// flavors, by value (they are cheap, two-word handles); +// - `virtual_any>` - looks the v-table pointer up once, at +// construction. The Concept needs `relaxed` for virtual_any's default +// constructor and assignment, and `copy_constructible<>` for copies. +// +// The rvalue-reference flavor (`any`), and placeholders +// other than `_self`, are not supported. + +namespace boost::openmethod { + +namespace detail { + +// Classification of the placeholder of an any. `T = _self` +// (or any non-reference placeholder): the any owns the value. `T = +// _self&`: non-owning handle to a mutable referent. Anything else +// (`const _self&`, `_self&&`) is treated as binding a value that may not +// be mutated or moved from. +template +constexpr bool te_owning = !std::is_reference_v; + +template +constexpr bool te_mutable_bound = std::is_lvalue_reference_v && + !std::is_const_v>; + +// Does U, an overrider parameter type, require mutable access? +template +constexpr bool te_mutable_target = std::is_lvalue_reference_v && + !std::is_const_v>; + +// Is U, an overrider parameter type, the `any` itself (by value or by +// any reference category)? Then the argument is passed through +// unchanged - the catch-all overrider case - instead of going through +// any_cast, which would throw unless the any contains an any. +template +constexpr bool te_pass_through = + std::is_same_v>, Any>; + +// The canonical root class for a Concept: the owning flavor. All the +// virtual_traits below use it as their virtual_type, whatever the +// flavor of the parameter, so methods, overriders and +// use_type_erasure_types agree on a single registered root per Concept. +template +using type_erasure_root = boost::type_erasure::any< + typename boost::type_erasure::concept_of::type>; + +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 {}; + +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::false_type { + static_assert( + false_t, "an owning type_erasure::any must be passed by reference"); +}; + +} // namespace detail + +//! Specialize virtual_traits for `const boost::type_erasure::any&`. +//! +//! Dispatch is based on the type of the value bound to the `any`, +//! obtained via `boost::type_erasure::typeid_of`. `Concept` must contain +//! `boost::type_erasure::typeid_<>`; `relaxed` implies it. +//! +//! This specialization serves the owning flavor (`any`) and, +//! through a const wrapper, the reference flavors. +//! +//! @tparam C The `any`'s Concept. +//! @tparam T The `any`'s placeholder. +//! @tparam Registry A @ref registry. +template +struct virtual_traits&, Registry> { + //! The type used for dispatch: the owning flavor for `C`. + using virtual_type = boost::type_erasure::any; + + //! Returns a const reference to the `any` argument. + //! @param arg A reference to an `any`. + //! @return A const reference to `arg`. + static auto peek(const boost::type_erasure::any& arg) + -> const boost::type_erasure::any& { + return arg; + } + + //! Returns a *reference* to a v-table pointer for the bound value. + //! + //! Looks up the @ref type_id returned by + //! `boost::type_erasure::typeid_of` in the registry's `vptr` policy. + //! + //! @param arg A reference to a const `any`. + //! @return A reference to the v-table pointer for the bound value. + static auto + vptr(const boost::type_erasure::any& arg) -> const vptr_type& { + return Registry::vptr::vptr(&boost::type_erasure::typeid_of(arg)); + } + + //! Cast to a type. + //! + //! Extracts the bound value using `boost::type_erasure::any_cast`. + //! Since the `any` is const, `U` can be a mutable reference only for + //! the mutable-reference flavor (`any`), whose + //! referent stays mutable through a const wrapper. Rvalue references + //! are never allowed; the overloads are removed from the overload + //! set. + //! + //! @tparam U The target type (e.g. `const Dog&`, `Dog`). + //! @param arg A reference to a const `any` method argument. + //! @return The value bound to `arg`, cast to `U`. + template< + typename U, + typename = std::enable_if_t< + !std::is_rvalue_reference_v && + (!detail::te_mutable_target || detail::te_mutable_bound)>> + static auto + cast(const boost::type_erasure::any& arg) -> decltype(auto) { + if constexpr (detail::te_pass_through< + U, boost::type_erasure::any>) { + return (arg); + } else { + return boost::type_erasure::any_cast(arg); + } + } +}; + +//! Specialize virtual_traits for `boost::type_erasure::any&` (mutable +//! reference). +//! +//! Dispatch is based on the type of the value bound to the `any`, +//! obtained via `boost::type_erasure::typeid_of`. `Concept` must contain +//! `boost::type_erasure::typeid_<>`; `relaxed` implies it. +//! +//! @tparam C The `any`'s Concept. +//! @tparam T The `any`'s placeholder. +//! @tparam Registry A @ref registry. +template +struct virtual_traits&, Registry> { + //! The type used for dispatch: the owning flavor for `C`. + using virtual_type = boost::type_erasure::any; + + //! Returns a const reference to the `any` argument. + //! @param arg A reference to an `any`. + //! @return A const reference to `arg`. + static auto peek(const boost::type_erasure::any& arg) + -> const boost::type_erasure::any& { + return arg; + } + + //! Returns a *reference* to a v-table pointer for the bound value. + //! + //! Looks up the @ref type_id returned by + //! `boost::type_erasure::typeid_of` in the registry's `vptr` policy. + //! + //! @param arg A reference to an `any`. + //! @return A reference to the v-table pointer for the bound value. + static auto + vptr(const boost::type_erasure::any& arg) -> const vptr_type& { + return Registry::vptr::vptr(&boost::type_erasure::typeid_of(arg)); + } + + //! Cast to a type. + //! + //! Extracts the bound value using `boost::type_erasure::any_cast`. + //! Supports mutable references (e.g. `Dog&`), except through the + //! const-reference flavor (`any`). `U` cannot + //! be an rvalue reference: moving the value out must go through an + //! explicit rvalue-reference parameter. The disallowed overloads are + //! removed from the overload set. + //! + //! @tparam U The target type (e.g. `Dog&`, `const Dog&`, `Dog`). + //! @param arg A mutable reference to the `any` method argument. + //! @return The value bound to `arg`, cast to `U`. + template< + typename U, + typename = std::enable_if_t< + !std::is_rvalue_reference_v && + (!detail::te_mutable_target || detail::te_owning || + detail::te_mutable_bound)>> + static auto cast(boost::type_erasure::any& arg) -> decltype(auto) { + if constexpr (detail::te_pass_through< + U, boost::type_erasure::any>) { + return (arg); + } else { + return boost::type_erasure::any_cast(arg); + } + } +}; + +//! Specialize virtual_traits for `boost::type_erasure::any&&` (xvalue +//! reference). +//! +//! Dispatch is based on the type of the value bound to the `any`, +//! obtained via `boost::type_erasure::typeid_of`. `Concept` must contain +//! `boost::type_erasure::typeid_<>`; `relaxed` implies it. +//! +//! @tparam C The `any`'s Concept. +//! @tparam T The `any`'s placeholder. +//! @tparam Registry A @ref registry. +template +struct virtual_traits&&, Registry> { + //! The type used for dispatch: the owning flavor for `C`. + using virtual_type = boost::type_erasure::any; + + //! Returns a const reference to the `any` argument. + //! @param arg A reference to an `any`. + //! @return A const reference to `arg`. + static auto peek(const boost::type_erasure::any& arg) + -> const boost::type_erasure::any& { + return arg; + } + + //! Returns a *reference* to a v-table pointer for the bound value. + //! + //! Looks up the @ref type_id returned by + //! `boost::type_erasure::typeid_of` in the registry's `vptr` policy. + //! + //! @param arg A reference to a const `any`. + //! @return A reference to the v-table pointer for the bound value. + static auto + vptr(const boost::type_erasure::any& arg) -> const vptr_type& { + return Registry::vptr::vptr(&boost::type_erasure::typeid_of(arg)); + } + + //! Cast to a type. + //! + //! Extracts the bound value using `boost::type_erasure::any_cast`. + //! `boost::type_erasure::any_cast` has no rvalue overload, so, for an + //! rvalue-reference `U`, the result of a mutable-reference cast is + //! moved - only for the owning flavor, since the rvalue-ness of a + //! reference wrapper says nothing about the referent. Casting to a + //! value also moves for the owning flavor, and copies otherwise. The + //! disallowed overloads are removed from the overload set. + //! + //! @tparam U The target type (e.g. `Dog&&`, `const Dog&`, `Dog`). + //! @param arg An rvalue reference to the `any` method argument. + //! @return The value bound to `arg`, cast to `U`. + template< + typename U, + typename = std::enable_if_t< + (!std::is_rvalue_reference_v || detail::te_owning) && + (!detail::te_mutable_target || detail::te_owning || + detail::te_mutable_bound)>> + static auto cast(boost::type_erasure::any&& arg) -> decltype(auto) { + if constexpr (detail::te_pass_through< + U, boost::type_erasure::any>) { + return std::move(arg); + } else if constexpr (std::is_rvalue_reference_v) { + return std::move( + boost::type_erasure::any_cast&>( + arg)); + } else if constexpr (!std::is_reference_v && detail::te_owning) { + return U(std::move(boost::type_erasure::any_cast(arg))); + } else { + return boost::type_erasure::any_cast(arg); + } + } +}; + +//! Specialize virtual_traits for the mutable reference-wrapper flavor, +//! `boost::type_erasure::any`, passed by value. +//! +//! The reference flavors are cheap, two-word handles; passing them by +//! value is the idiomatic way to use them as parameters. Dispatch is on +//! the type *bound at construction*, obtained via +//! `boost::type_erasure::typeid_of` - not the C++ RTTI dynamic type of +//! the referent. `Concept` must contain `boost::type_erasure::typeid_<>`; +//! `relaxed` implies it. +//! +//! @tparam C The `any`'s Concept. +//! @tparam T The referent placeholder (`_self` for `any`). +//! @tparam Registry A @ref registry. +template +struct virtual_traits, Registry> { + //! The type used for dispatch: the owning flavor for `C`. + using virtual_type = boost::type_erasure::any; + + //! Returns a const reference to the `any` argument. + //! @param arg A reference to an `any`. + //! @return A const reference to `arg`. + static auto peek(const boost::type_erasure::any& arg) + -> const boost::type_erasure::any& { + return arg; + } + + //! Returns a *reference* to a v-table pointer for the bound value. + //! + //! Looks up the @ref type_id returned by + //! `boost::type_erasure::typeid_of` in the registry's `vptr` policy. + //! + //! @param arg A reference to a const `any`. + //! @return A reference to the v-table pointer for the bound value. + static auto + vptr(const boost::type_erasure::any& arg) -> const vptr_type& { + return Registry::vptr::vptr(&boost::type_erasure::typeid_of(arg)); + } + + //! Cast to a type. + //! + //! Extracts the referent using `boost::type_erasure::any_cast`. + //! Supports mutable references (e.g. `Dog&`); modifications through + //! the result are visible through the referent. `U` cannot be an + //! rvalue reference - the referent is borrowed, not owned; the + //! overloads are removed from the overload set. + //! + //! @tparam U The target type (e.g. `Dog&`, `const Dog&`, `Dog`). + //! @param arg The reference-wrapper `any` method argument. + //! @return The value bound to `arg`, cast to `U`. + template< + typename U, typename = std::enable_if_t>> + static auto cast(boost::type_erasure::any arg) -> decltype(auto) { + if constexpr (detail::te_pass_through< + U, boost::type_erasure::any>) { + // by value: a reference would dangle when this function's + // parameter goes out of scope + return arg; + } else { + return boost::type_erasure::any_cast(arg); + } + } +}; + +//! Specialize virtual_traits for the const reference-wrapper flavor, +//! `boost::type_erasure::any`, passed by value. +//! +//! The reference flavors are cheap, two-word handles; passing them by +//! value is the idiomatic way to use them as parameters. Dispatch is on +//! the type *bound at construction*, obtained via +//! `boost::type_erasure::typeid_of` - not the C++ RTTI dynamic type of +//! the referent. `Concept` must contain `boost::type_erasure::typeid_<>`; +//! `relaxed` implies it. +//! +//! @tparam C The `any`'s Concept. +//! @tparam T The referent placeholder (`_self` for +//! `any`). +//! @tparam Registry A @ref registry. +template +struct virtual_traits, Registry> { + //! The type used for dispatch: the owning flavor for `C`. + using virtual_type = boost::type_erasure::any; + + //! Returns a const reference to the `any` argument. + //! @param arg A reference to an `any`. + //! @return A const reference to `arg`. + static auto peek(const boost::type_erasure::any& arg) + -> const boost::type_erasure::any& { + return arg; + } + + //! Returns a *reference* to a v-table pointer for the bound value. + //! + //! Looks up the @ref type_id returned by + //! `boost::type_erasure::typeid_of` in the registry's `vptr` policy. + //! + //! @param arg A reference to a const `any`. + //! @return A reference to the v-table pointer for the bound value. + static auto + vptr(const boost::type_erasure::any& arg) -> const vptr_type& { + return Registry::vptr::vptr(&boost::type_erasure::typeid_of(arg)); + } + + //! Cast to a type. + //! + //! Extracts the referent using `boost::type_erasure::any_cast`. Since + //! the referent is const, `U` must be a value or a const reference; + //! the other overloads are removed from the overload set. + //! + //! @tparam U The target type (e.g. `const Dog&`, `Dog`). + //! @param arg The reference-wrapper `any` method argument. + //! @return The value bound to `arg`, cast to `U`. + template< + typename U, + typename = std::enable_if_t< + !std::is_rvalue_reference_v && !detail::te_mutable_target>> + static auto + cast(boost::type_erasure::any arg) -> decltype(auto) { + if constexpr (detail::te_pass_through< + U, boost::type_erasure::any>) { + // by value: a reference would dangle when this function's + // parameter goes out of scope + return arg; + } else { + return boost::type_erasure::any_cast(arg); + } + } +}; + +//! Register the types that a `boost::type_erasure::any` virtual parameter +//! may contain. +//! +//! Registers the owning flavor of `Any` (i.e. +//! `any::type>`) as a class, and each `T` as a class +//! derived from it. This makes the bound types visible to the dispatch +//! machinery, which resolves a call on the `type_id` returned by +//! `boost::type_erasure::typeid_of`. `Any` may be spelled with any +//! flavor; the root is normalized to the owning flavor, which is also +//! what the virtual_traits use, whatever the flavor of the method +//! parameter. +//! +//! @tparam Any A `boost::type_erasure::any` type. +//! @tparam T... The types that may be bound to the `any`, optionally +//! followed by a @ref registry. +template +struct use_type_erasure_types + : detail::use_any_types_aux< + typename detail::extract_registry::registry, + detail::type_erasure_root, + typename detail::extract_registry::others> {}; + +// The primary final_virtual_ptr would silently use the static v-table +// pointer of the any class itself - the root -, not the bound value's. +// Delete the combination. Both call forms need covering: the (C, T)-only +// templates catch calls that deduce the default registry, and the +// Registry-first templates catch explicit-registry calls; both are more +// specialized than the primary's forwarding-reference parameter. + +template +void final_virtual_ptr(const boost::type_erasure::any&) = delete; +template +void final_virtual_ptr(boost::type_erasure::any&) = delete; +template +void final_virtual_ptr(boost::type_erasure::any&&) = delete; +template +void final_virtual_ptr(const boost::type_erasure::any&) = delete; +template +void final_virtual_ptr(boost::type_erasure::any&) = delete; +template +void final_virtual_ptr(boost::type_erasure::any&&) = delete; + +namespace aliases { +using boost::openmethod::use_type_erasure_types; +} // namespace aliases + +} // namespace boost::openmethod + +#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6678cd47..4799b98c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -169,6 +169,13 @@ openmethod_compile_fail_test( # "attempting to reference a deleted function" on MSVC. openmethod_compile_fail_test( compile_fail_final_virtual_ptr_std_any "deleted function") +openmethod_compile_fail_test( + compile_fail_type_erasure_by_value + "an owning type_erasure::any must be passed by reference") +openmethod_compile_fail_test( + compile_fail_type_erasure_const_ref_to_mutable_ref "no matching") +openmethod_compile_fail_test( + compile_fail_final_virtual_ptr_type_erasure "deleted function") if (TARGET Boost::dll) add_subdirectory(dynamic_loading) diff --git a/test/Jamfile b/test/Jamfile index 10ab8c56..841e5643 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -21,6 +21,7 @@ project /boost/openmethod//boost_openmethod /boost/any//boost_any + /boost/type_erasure//boost_type_erasure extra diff --git a/test/compile_fail_final_virtual_ptr_type_erasure.cpp b/test/compile_fail_final_virtual_ptr_type_erasure.cpp new file mode 100644 index 00000000..9660b965 --- /dev/null +++ b/test/compile_fail_final_virtual_ptr_type_erasure.cpp @@ -0,0 +1,33 @@ +// Copyright (c) 2018-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include + +#include +#include + +#include +#include + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +using Concept = boost::mpl::vector, te::relaxed>; +using erased = te::any; + +struct Dog { + std::string name; +}; + +BOOST_OPENMETHOD_REGISTER(use_type_erasure_types); + +int main() { + // The primary final_virtual_ptr would use the static v-table pointer + // of the any class itself - the root -, not the bound value's. The + // combination is deleted; use virtual_any instead. + erased spot(Dog{"Spot"}); + final_virtual_ptr(spot); + return 0; +} diff --git a/test/compile_fail_type_erasure_by_value.cpp b/test/compile_fail_type_erasure_by_value.cpp new file mode 100644 index 00000000..9efe2b26 --- /dev/null +++ b/test/compile_fail_type_erasure_by_value.cpp @@ -0,0 +1,34 @@ +// Copyright (c) 2018-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include + +#include +#include + +#include +#include + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +using Concept = boost::mpl::vector, te::relaxed>; +using erased = te::any; + +struct Dog { + std::string name; +}; + +BOOST_OPENMETHOD_REGISTER(use_type_erasure_types); + +// The owning flavor must be passed by reference: by value, it would copy +// the `any` - and its payload - on every call. (The reference-wrapper +// flavors, any and any, may be passed by +// value.) +BOOST_OPENMETHOD(name, (virtual_), std::string); + +int main() { + return 0; +} diff --git a/test/compile_fail_type_erasure_const_ref_to_mutable_ref.cpp b/test/compile_fail_type_erasure_const_ref_to_mutable_ref.cpp new file mode 100644 index 00000000..a64b0773 --- /dev/null +++ b/test/compile_fail_type_erasure_const_ref_to_mutable_ref.cpp @@ -0,0 +1,37 @@ +// Copyright (c) 2018-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include + +#include +#include + +#include +#include + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +using Concept = boost::mpl::vector, te::relaxed>; +using erased = te::any; + +struct Dog { + std::string name; +}; + +BOOST_OPENMETHOD_REGISTER(use_type_erasure_types); + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +// The `any` is const and owns its value, so the overrider cannot take a +// mutable reference to it; the `cast` overload is removed from the +// overload set. +BOOST_OPENMETHOD_OVERRIDE(name, (Dog & dog), std::string) { + return dog.name; +} + +int main() { + return 0; +} diff --git a/test/test_dispatch_type_erasure.cpp b/test/test_dispatch_type_erasure.cpp new file mode 100644 index 00000000..9927671a --- /dev/null +++ b/test/test_dispatch_type_erasure.cpp @@ -0,0 +1,291 @@ +// Copyright (c) 2018-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +#include + +#include +#include +#include + +#include +#include +#include + +#define BOOST_TEST_MODULE openmethod +#include + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +// `relaxed` implies typeid_<>, on which typeid_of and any_cast - thus +// dispatch - rely; no explicit typeid_<> needed. +using Concept = boost::mpl::vector, te::relaxed>; +using erased = te::any; +using erased_ref = te::any; +using erased_cref = te::any; + +static_assert(detail::has_vptr< + virtual_traits, const erased&>); + +#define MAKE_CLASSES() \ + struct Dog { \ + std::string name; \ + }; \ + \ + BOOST_OPENMETHOD_REGISTER( \ + use_type_erasure_types); + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as const any& (const ref) + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { + return name; +} + +// A catch-all overrider may take the `any` itself; the argument is passed +// through unchanged. +BOOST_OPENMETHOD_OVERRIDE(name, (const erased& arg), std::string) { + return te::is_empty(arg) ? "nothing" : "something"; +} + +BOOST_AUTO_TEST_CASE(type_erasure_by_const_ref) { + initialize(trace()); + + const erased spot(Dog{"Spot"}); + const erased felix(std::string{"Felix the cat"}); + const erased answer(42); + + BOOST_TEST(name(spot) == "Spot the dog"); + BOOST_TEST(name(felix) == "Felix the cat"); + // `int` is registered but has no specific overrider: the catch-all, + // registered for the root, applies + BOOST_TEST(name(answer) == "something"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as any& (mutable ref) + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(bump, (virtual_), std::string); + +// BOOST_OPENMETHOD_OVERRIDE cannot express this: a temporary `any` binds +// to `const any&` and to `any&&`, but nothing binds to a mutable lvalue +// reference. Register directly via method<...>::override instead - +// the primitive the macro itself expands to. + +using bump_method = + BOOST_OPENMETHOD_TYPE(bump, (virtual_), std::string); + +auto bump_dog(Dog& dog) -> std::string { + dog.name += " Jr."; + return dog.name + " the dog"; +} + +auto bump_int(int& value) -> std::string { + ++value; + return "bumped"; +} + +BOOST_OPENMETHOD_REGISTER(bump_method::override); +BOOST_OPENMETHOD_REGISTER(bump_method::override); + +BOOST_AUTO_TEST_CASE(type_erasure_by_mutable_ref) { + initialize(trace()); + + erased spot(Dog{"Spot"}); + BOOST_TEST(bump(spot) == "Spot Jr. the dog"); + // the mutation is visible through the `any` + BOOST_TEST(te::any_cast(spot).name == "Spot Jr."); + + erased answer(41); + BOOST_TEST(bump(answer) == "bumped"); + BOOST_TEST(te::any_cast(answer) == 42); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as any&& (xvalue ref) + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(steal, (virtual_), std::string); + +// boost::type_erasure::any_cast has no rvalue overload; the trait moves +// the result of a mutable-reference cast, because the `any` owns its +// value. +BOOST_OPENMETHOD_OVERRIDE(steal, (Dog && dog), std::string) { + Dog stolen(std::move(dog)); + return stolen.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(steal, (std::string && name), std::string) { + std::string stolen(std::move(name)); + return stolen; +} + +BOOST_AUTO_TEST_CASE(type_erasure_by_xvalue_ref) { + initialize(trace()); + + erased spot(Dog{"Spot"}); + BOOST_TEST(steal(std::move(spot)) == "Spot the dog"); + // the overrider moved the name out; the `any` still owns the Dog + BOOST_TEST(!te::is_empty(spot)); + BOOST_TEST(te::any_cast(spot).name == ""); + + erased felix(std::string{"Felix the cat"}); + BOOST_TEST(steal(std::move(felix)) == "Felix the cat"); + BOOST_TEST(te::any_cast(felix) == ""); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as any - the mutable reference-wrapper +// flavor - by value + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(poke, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(poke, (Dog & dog), std::string) { + dog.name += "!"; + return dog.name; +} + +BOOST_OPENMETHOD_OVERRIDE(poke, (int& value), std::string) { + ++value; + return "poked"; +} + +BOOST_AUTO_TEST_CASE(type_erasure_ref_wrapper_by_value) { + initialize(trace()); + + // the wrapper is a cheap handle; mutations reach the referents + Dog snoopy{"Snoopy"}; + int count = 41; + + BOOST_TEST(poke(erased_ref(snoopy)) == "Snoopy!"); + BOOST_TEST(snoopy.name == "Snoopy!"); + + BOOST_TEST(poke(erased_ref(count)) == "poked"); + BOOST_TEST(count == 42); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as any - the const reference-wrapper +// flavor - by value + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +// the catch-all receives a copy of the wrapper - still a cheap handle +BOOST_OPENMETHOD_OVERRIDE(name, (erased_cref arg), std::string) { + return te::is_empty(arg) ? "nothing" : "something"; +} + +BOOST_AUTO_TEST_CASE(type_erasure_cref_wrapper_by_value) { + initialize(trace()); + + Dog snoopy{"Snoopy"}; + const int count = 42; + + BOOST_TEST(name(erased_cref(snoopy)) == "Snoopy the dog"); + BOOST_TEST(name(erased_cref(count)) == "something"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// virtual_any over a type_erasure any: the v-table pointer is looked up +// once, at construction - or set statically when the contained type is +// known - and dispatch does not hash typeid_of on every call + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (const virtual_any&), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_AUTO_TEST_CASE(type_erasure_virtual_any) { + initialize(trace()); + + // from an `any`: runtime lookup via typeid_of + erased spot_any(Dog{"Spot"}); + virtual_any spot = spot_any; + BOOST_TEST(spot.vptr() == default_registry::static_vptr); + BOOST_TEST(name(spot) == "Spot the dog"); + + // from a value: the v-table pointer is set statically + virtual_any rex = Dog{"Rex"}; + BOOST_TEST(rex.vptr() == default_registry::static_vptr); + BOOST_TEST(name(rex) == "Rex the dog"); + + auto snoopy = make_any_virtual(Dog{"Snoopy"}); + BOOST_TEST(snoopy.vptr() == default_registry::static_vptr); + BOOST_TEST(name(snoopy) == "Snoopy the dog"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// indirect vptrs + +struct Dog { + std::string name; +}; + +BOOST_OPENMETHOD_REGISTER( + use_type_erasure_types); + +using name_method = method< + struct name_id, std::string(virtual_), indirect_registry>; + +auto name_dog(const Dog& dog) -> std::string { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_REGISTER(name_method::override); + +BOOST_AUTO_TEST_CASE(type_erasure_indirect_vptr) { + initialize(); + + const erased spot(Dog{"Spot"}); + BOOST_TEST(name_method::fn(spot) == "Spot the dog"); + + virtual_any rex = Dog{"Rex"}; + BOOST_TEST(rex.vptr() == indirect_registry::static_vptr); + BOOST_TEST(name_method::fn(rex.get()) == "Rex the dog"); +} +} // namespace BOOST_OPENMETHOD_GENSYM From 78143c84d453e7d724891a16ed80eedc0abd6e14 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 8 Aug 2026 12:54:55 -0400 Subject: [PATCH 14/64] fix infinite recursion in virtual_any's vptr friend on MSVC MSVC's /std:c++17 does not imply /permissive-, and in permissive mode MSVC injects friend functions into the enclosing namespace, where detail::acquire_vptr's unqualified call finds them. Called with a plain `Any`, boost_openmethod_vptr was viable through virtual_any's implicit converting constructor - which acquires the v-table pointer, calling the friend again. The recursion is unconditional: release builds failed with warning C4717 under /WX, debug builds overflowed the stack at runtime. Constrain the friend's parameter to a deduced type that must be exactly this virtual_any, so no implicit conversion can make it viable. Co-Authored-By: Claude Fable 5 --- include/boost/openmethod/interop/virtual_any.hpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/include/boost/openmethod/interop/virtual_any.hpp b/include/boost/openmethod/interop/virtual_any.hpp index 5afe7036..7f61104a 100644 --- a/include/boost/openmethod/interop/virtual_any.hpp +++ b/include/boost/openmethod/interop/virtual_any.hpp @@ -225,8 +225,18 @@ class virtual_any { } #ifndef __MRDOCS__ - friend auto - boost_openmethod_vptr(const virtual_any& va, Registry*) -> vptr_type { + // The parameter is deduced, and constrained to be exactly this + // `virtual_any`, so that the function is not viable for a type that + // is merely convertible to it. MSVC, in its default (permissive) + // mode, injects friend functions into the enclosing namespace, where + // ordinary lookup finds them. An unconstrained `const virtual_any&` + // parameter would then make this a candidate for a plain `Any`, + // which converts implicitly to `virtual_any` - and the conversion + // acquires the v-table pointer, which calls this function, ad + // infinitum. + template + friend auto boost_openmethod_vptr(const Self& va, Registry*) + -> std::enable_if_t, vptr_type> { return detail::unbox_vptr(va.vp); } #endif From 9dcba28870d119d3066879a96d00c67e04278dab Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 8 Aug 2026 13:35:22 -0400 Subject: [PATCH 15/64] doc: an Interoperation page, and reference examples for the `any`s `virtual_any` shipped with tests but no narrative documentation: nothing in the nav mentioned `any`, no guide page covered it, and the reference pages carried no examples. Add an "Interoperation with Other Libraries" page under Advanced Features, structured to take a `boost::intrusive_ptr` section later. It covers, for `std::any`: why dispatch on an `any` at all, registering the contained types, `virtual_std_any` and where its v-table pointer comes from, what overriders receive, the three reference categories and why the macro cannot express the mutable one, and when to prefer a plain `virtual_` instead. `boost::any` gets a mention rather than a repeat. The page's example is a new top-level doc example. The reference examples are regions of doc/modules/ROOT/snippets/virtual_any.cpp, pulled in with `include:` markers, so they are compiled and run like the rest. Co-Authored-By: Claude Fable 5 --- doc/modules/ROOT/examples/virtual_any.cpp | 73 +++++++ doc/modules/ROOT/nav.adoc | 1 + doc/modules/ROOT/pages/interop.adoc | 141 +++++++++++++ doc/modules/ROOT/snippets/virtual_any.cpp | 193 ++++++++++++++++++ .../boost/openmethod/interop/boost_any.hpp | 12 ++ include/boost/openmethod/interop/std_any.hpp | 12 ++ .../boost/openmethod/interop/virtual_any.hpp | 19 ++ 7 files changed, 451 insertions(+) create mode 100644 doc/modules/ROOT/examples/virtual_any.cpp create mode 100644 doc/modules/ROOT/pages/interop.adoc create mode 100644 doc/modules/ROOT/snippets/virtual_any.cpp diff --git a/doc/modules/ROOT/examples/virtual_any.cpp b/doc/modules/ROOT/examples/virtual_any.cpp new file mode 100644 index 00000000..83619e30 --- /dev/null +++ b/doc/modules/ROOT/examples/virtual_any.cpp @@ -0,0 +1,73 @@ +// Copyright (c) 2018-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) + +// clang-format off + +// tag::content[] +#include +#include +#include + +#include +#include + +using namespace boost::openmethod; + +struct Dog { + Dog(std::string name) : name(std::move(name)) {} + std::string name; +}; + +struct Cat { + Cat(std::string name) : name(std::move(name)) {} + std::string name; +}; + +// `std::any` becomes the common base of the types it may contain. +BOOST_OPENMETHOD_REGISTER(use_std_any_types); + +BOOST_OPENMETHOD(poke, (const virtual_std_any&), std::string); + +// An overrider takes the contained value... +BOOST_OPENMETHOD_OVERRIDE(poke, (const Dog& dog), std::string) { + return dog.name + " barks"; +} + +BOOST_OPENMETHOD_OVERRIDE(poke, (const Cat& cat), std::string) { + return cat.name + " hisses"; +} + +// ...or the `virtual_any` itself, which makes it a catch-all. +BOOST_OPENMETHOD_OVERRIDE(poke, (const virtual_std_any& value), std::string) { + return value.get().has_value() ? "it does nothing" : "nothing happens"; +} + +#include + +int main() { + initialize(); + + // From an existing `any`: the v-table pointer is looked up from the type + // of the value it contains. + std::any snoopy_any = Dog("Snoopy"); + virtual_std_any snoopy = snoopy_any; + + // From a value: the type is known at compile time, so the v-table pointer + // is read from a static variable, with no lookup. + virtual_std_any felix = Cat("Felix"); + + // Same, constructing the value in place. + auto hector = make_std_any_virtual("Hector"); + + std::cout << poke(snoopy) << "\n"; // Snoopy barks + std::cout << poke(felix) << "\n"; // Felix hisses + std::cout << poke(hector) << "\n"; // Hector barks + + // `int` is registered, but has no overrider of its own: the catch-all + // applies. The value converts to a temporary `virtual_std_any` at the + // call site. + std::cout << poke(42) << "\n"; // it does nothing +} +// end::content[] diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index b6b8b6c2..5f6dc368 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -13,6 +13,7 @@ ** xref:custom_rtti.adoc[Custom RTTI] ** xref:error_handling.adoc[Error Handling] ** xref:virtual_ptr_alt.adoc[Virtual Pointer Alternatives] +** xref:interop.adoc[Interoperation with Other Libraries] ** xref:shared_libraries.adoc[Shared Libraries] * xref:reference:index.adoc[Reference] ** xref:ref_headers.adoc[Headers] diff --git a/doc/modules/ROOT/pages/interop.adoc b/doc/modules/ROOT/pages/interop.adoc new file mode 100644 index 00000000..25f36bfa --- /dev/null +++ b/doc/modules/ROOT/pages/interop.adoc @@ -0,0 +1,141 @@ + +[#interop] +## Interoperation with Other Libraries + +Some libraries hand us a value whose type is not visible in the static type of +the variable that holds it - a type-erased container, or a pointer class of +their own. This section covers the constructs that let a method look through +such a wrapper and dispatch on what is really inside. + +### `any` + +An `any` holds a value of almost any type, and remembers which type that is. +That is precisely what a method needs in order to pick an overrider. OpenMethod +can thus dispatch on the type _contained_ in an `any`, in effect treating a set +of otherwise unrelated types as a hierarchy rooted at `std::any`. The types need +not be polymorphic, and need not be related to one another - which makes this a +way of adding behavior to types we do not own, including built-in types. + +Support is provided by ``. It is not +included by ``, so it must be included explicitly. + +Dispatch works on classes known to a registry, so the types the `any` may +contain have to be registered. cpp:use_std_any_types[] does that, registering +`std::any` as a class, and each of the types as a class derived from it. A type +that is not registered cannot be dispatched on; a call with such a value in the +`any` is a cpp:missing_class[] error - see +xref:error_handling.adoc[Error Handling]. + +cpp:virtual_std_any[] - an alias for `virtual_any` - is to an `any` +what cpp:virtual_ptr[] is to a pointer: it bundles the `any` with a pointer to +the v-table for the value it contains, so a call does not have to look that +v-table up. Unlike `virtual_ptr`, it _owns_ the object: the `any` is held by +value. + +Overriders receive the _contained_ value, by a reference of a compatible +category - not the wrapper. An overrider may also take the `virtual_std_any` +itself, unchanged; since every registered type derives from `std::any`, such an +overrider is a catch-all, applying to any contained type that has no more +specific overrider: + +[source,c++] +---- +include::example$virtual_any.cpp[tag=content] +---- + +#### Where the v-table pointer comes from + +A `virtual_any` acquires its v-table pointer once, when it is created, and +maintains it across assignment and `emplace`. There are two ways it can do so: + +- From an existing `any`, the contained type is known only at run time, so the +v-table pointer is looked up in a hash table, keyed on the type of the contained +value. + +- From a value, or from cpp:make_std_any_virtual[], or from `emplace`, the +contained type is known at compile time, so the v-table pointer is simply read +from a static variable - no lookup at all. + +The second form is the one to prefer where we have the choice. + +#### Reference categories + +A `virtual_std_any` method parameter must be a reference - passing it by value +would copy the `any`, and the value inside it, on every call. All three +reference categories are supported, and they determine what the overriders may +take: + +[cols="1,2"] +|=== +| Method parameter | Overrider parameter + +| `const virtual_std_any&` +| `const Dog&`, `Dog` + +| `virtual_std_any&` +| `Dog&`, `const Dog&`, `Dog` + +| `virtual_std_any&&` +| `Dog&&`, `const Dog&`, `Dog` +|=== + +A value converts implicitly to a `virtual_std_any`, so `poke(42)` in the example +above creates a temporary at the call site. That temporary binds to `const +virtual_std_any&` and to `virtual_std_any&&`, but nothing binds to a mutable +lvalue reference. Consequently +xref:reference:BOOST_OPENMETHOD_OVERRIDE.adoc[BOOST_OPENMETHOD_OVERRIDE], which +locates the method by checking that the overrider's parameters can be passed to +the method's forwarder, cannot be used with a `virtual_std_any&` method. Such +overriders are registered with the core API instead - the primitive the macro +itself expands to: + +```c++ +using bump_method = BOOST_OPENMETHOD_TYPE(bump, (virtual_std_any&), std::string); + +auto bump_dog(Dog& dog) -> std::string { + dog.name += " Jr."; + return dog.name; +} + +BOOST_OPENMETHOD_REGISTER(bump_method::override); +``` + +#### Dispatching on a plain `any` + +`virtual_std_any` is not mandatory. A `std::any` can also be used directly in a +virtual parameter, wrapped in `virtual_`, as described in +xref:virtual_ptr_alt.adoc[Alternatives to virtual_ptr]: + +```c++ +BOOST_OPENMETHOD(poke, (virtual_), std::string); +``` + +The overriders are written exactly as before, and the same `use_std_any_types` +registration applies. The difference is where the v-table pointer comes from: +there is nowhere to cache it, so every call performs the hash table lookup. + +Which to use: + +- `virtual_`, when the `any` comes from elsewhere - an existing +API, a container of `std::any` - and is dispatched on once. It adds nothing to +the `any`, and requires no change to the code that produces it. + +- `virtual_std_any`, when the same value is dispatched on repeatedly, or when we +create it ourselves and its type is statically known. The lookup then happens +once, or not at all. + +For the same reason, cpp:final_virtual_ptr[] is _deleted_ for `std::any`: it +would silently produce the v-table of the `any` root class rather than the one +for the contained value. + +#### `boost::any` + +`boost::any` is supported as well, by +``, with cpp:use_boost_any_types[], +cpp:virtual_boost_any[] and cpp:make_boost_any_virtual[] - the exact +counterparts of the constructs above. The two root classes are distinct, so +`std::any` and `boost::any` may be used in the same program, and with the same +registry. + +cpp:virtual_any[] itself is generic: it can serve any type with an `any`-like +interface, given cpp:virtual_traits[] specializations for its reference types. diff --git a/doc/modules/ROOT/snippets/virtual_any.cpp b/doc/modules/ROOT/snippets/virtual_any.cpp new file mode 100644 index 00000000..7a093f46 --- /dev/null +++ b/doc/modules/ROOT/snippets/virtual_any.cpp @@ -0,0 +1,193 @@ +// Copyright (c) 2018-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +#include + +#include +#include +#include +#include +#include + +#define BOOST_TEST_MODULE openmethod +#include + +#include "capture.hpp" + +using namespace boost::openmethod; + +namespace std_any { + +// tag::classes[] +struct Dog { + Dog(std::string name) : name(std::move(name)) { + } + + std::string name; +}; + +struct Cat { + Cat(std::string name) : name(std::move(name)) { + } + + std::string name; +}; + +// `std::any` becomes the common base of the types it may contain. +BOOST_OPENMETHOD_REGISTER(use_std_any_types); +// end::classes[] + +// tag::method[] +BOOST_OPENMETHOD(poke, (const virtual_std_any&), std::string); + +// An overrider takes the contained value... +BOOST_OPENMETHOD_OVERRIDE(poke, (const Dog& dog), std::string) { + return dog.name + " barks"; +} + +BOOST_OPENMETHOD_OVERRIDE(poke, (const Cat& cat), std::string) { + return cat.name + " hisses"; +} + +// ...or the `virtual_any` itself, which makes it a catch-all. +BOOST_OPENMETHOD_OVERRIDE(poke, (const virtual_std_any& value), std::string) { + return value.get().has_value() ? "it does nothing" : "nothing happens"; +} +// end::method[] + +} // namespace std_any + +namespace boost_any { + +// tag::boost_classes[] +struct Dog { + Dog(std::string name) : name(std::move(name)) { + } + + std::string name; +}; + +// `boost::any` is a root class of its own, distinct from the one used for +// `std::any`, so both may be used in the same program and registry. +BOOST_OPENMETHOD_REGISTER(use_boost_any_types); + +BOOST_OPENMETHOD(poke, (const virtual_boost_any&), std::string); + +BOOST_OPENMETHOD_OVERRIDE(poke, (const Dog& dog), std::string) { + return dog.name + " barks"; +} +// end::boost_classes[] + +} // namespace boost_any + +BOOST_AUTO_TEST_CASE(std_any_examples) { + using namespace std_any; + + initialize(); + + { + capture_cout cout; + + // tag::dispatch[] + virtual_std_any snoopy = Dog("Snoopy"); + + std::cout << poke(snoopy) << "\n"; // Snoopy barks + + // `int` is registered, but has no overrider of its own, so the + // catch-all applies. The value converts to a temporary + // `virtual_std_any` at the call site. + std::cout << poke(42) << "\n"; // it does nothing + // end::dispatch[] + + BOOST_TEST(cout.str() == "Snoopy barks\nit does nothing\n"); + } + + { + capture_cout cout; + + // tag::from_any[] + std::any snoopy_any = Dog("Snoopy"); + + // the v-table pointer is looked up from the type of the value the + // `any` contains + virtual_std_any snoopy = snoopy_any; + + std::cout << poke(snoopy) << "\n"; // Snoopy barks + // end::from_any[] + + BOOST_TEST(cout.str() == "Snoopy barks\n"); + } + + { + capture_cout cout; + + // tag::from_value[] + // `Cat` is known at compile time, so the v-table pointer is read + // from a static variable - there is no lookup + virtual_std_any felix = Cat("Felix"); + + std::cout << poke(felix) << "\n"; // Felix hisses + // end::from_value[] + + BOOST_TEST(cout.str() == "Felix hisses\n"); + } + + { + capture_cout cout; + + // tag::emplace[] + virtual_std_any animal; + + animal.emplace("Felix"); + + std::cout << poke(animal) << "\n"; // Felix hisses + // end::emplace[] + + BOOST_TEST(cout.str() == "Felix hisses\n"); + } + + { + capture_cout cout; + + // tag::make_any_virtual[] + auto felix = make_any_virtual("Felix"); + + std::cout << poke(felix) << "\n"; // Felix hisses + // end::make_any_virtual[] + + BOOST_TEST(cout.str() == "Felix hisses\n"); + } + + { + capture_cout cout; + + // tag::make_std_any_virtual[] + auto snoopy = make_std_any_virtual("Snoopy"); + + std::cout << poke(snoopy) << "\n"; // Snoopy barks + // end::make_std_any_virtual[] + + BOOST_TEST(cout.str() == "Snoopy barks\n"); + } +} + +BOOST_AUTO_TEST_CASE(boost_any_examples) { + using namespace boost_any; + + initialize(); + + { + capture_cout cout; + + // tag::boost_dispatch[] + auto snoopy = make_boost_any_virtual("Snoopy"); + + std::cout << poke(snoopy) << "\n"; // Snoopy barks + // end::boost_dispatch[] + + BOOST_TEST(cout.str() == "Snoopy barks\n"); + } +} diff --git a/include/boost/openmethod/interop/boost_any.hpp b/include/boost/openmethod/interop/boost_any.hpp index 3e77d09e..ca633cdc 100644 --- a/include/boost/openmethod/interop/boost_any.hpp +++ b/include/boost/openmethod/interop/boost_any.hpp @@ -230,6 +230,11 @@ struct virtual_traits { //! //! @tparam T... The types that may be stored in the `any`, optionally //! followed by a @ref registry. +//! +//! @par Example +//! include:virtual_any.cpp#boost_classes;boost_dispatch +//! +//! @see [Interoperation with Other Libraries](xref:ROOT:interop.adoc) template struct use_boost_any_types : detail::use_class_aux< @@ -242,6 +247,8 @@ struct use_boost_any_types //! Alias for a `virtual_any`, in the default registry. //! //! With another registry, use `virtual_any` directly. +//! +//! @see [Interoperation with Other Libraries](xref:ROOT:interop.adoc) using virtual_boost_any = virtual_any; //! Create a new object and return a `virtual_boost_any` containing it. @@ -258,6 +265,11 @@ using virtual_boost_any = virtual_any; //! @param args Arguments to pass to the constructor of `Class`. //! @return A `virtual_any` containing a newly created //! `Class`. +//! +//! @par Example +//! include:virtual_any.cpp#boost_dispatch +//! +//! @see [Interoperation with Other Libraries](xref:ROOT:interop.adoc) template< class Class, class Registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY, typename... T> diff --git a/include/boost/openmethod/interop/std_any.hpp b/include/boost/openmethod/interop/std_any.hpp index 1b0bda96..6269e3c3 100644 --- a/include/boost/openmethod/interop/std_any.hpp +++ b/include/boost/openmethod/interop/std_any.hpp @@ -193,6 +193,11 @@ struct virtual_traits { //! //! @tparam T... The types that may be stored in the `any`, optionally //! followed by a @ref registry. +//! +//! @par Example +//! include:virtual_any.cpp#classes +//! +//! @see [Interoperation with Other Libraries](xref:ROOT:interop.adoc) template struct use_std_any_types : detail::use_class_aux< @@ -205,6 +210,8 @@ struct use_std_any_types //! Alias for a `virtual_any`, in the default registry. //! //! With another registry, use `virtual_any` directly. +//! +//! @see [Interoperation with Other Libraries](xref:ROOT:interop.adoc) using virtual_std_any = virtual_any; //! Create a new object and return a `virtual_std_any` containing it. @@ -221,6 +228,11 @@ using virtual_std_any = virtual_any; //! @param args Arguments to pass to the constructor of `Class`. //! @return A `virtual_any` containing a newly created //! `Class`. +//! +//! @par Example +//! include:virtual_any.cpp#make_std_any_virtual +//! +//! @see [Interoperation with Other Libraries](xref:ROOT:interop.adoc) template< class Class, class Registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY, typename... T> diff --git a/include/boost/openmethod/interop/virtual_any.hpp b/include/boost/openmethod/interop/virtual_any.hpp index 7f61104a..485f569d 100644 --- a/include/boost/openmethod/interop/virtual_any.hpp +++ b/include/boost/openmethod/interop/virtual_any.hpp @@ -57,6 +57,11 @@ struct is_virtual_any_aux> : std::true_type {}; //! //! @tparam Any An `any` type. //! @tparam Registry A @ref registry. +//! +//! @par Example +//! include:virtual_any.cpp#classes;method;dispatch +//! +//! @see [Interoperation with Other Libraries](xref:ROOT:interop.adoc) template class virtual_any { static constexpr bool use_indirect_vptrs = Registry::has_indirect_vptr; @@ -81,6 +86,9 @@ class virtual_any { //! value, using `virtual_traits::vptr`. //! //! @param other An `any`. + //! + //! @par Example + //! include:virtual_any.cpp#from_any virtual_any(const Any& other) : obj(other), vp(detail::box_vptr( detail::acquire_vptr(obj))) { @@ -106,6 +114,9 @@ class virtual_any { //! //! @tparam T The type of the value. //! @param value The value to store. + //! + //! @par Example + //! include:virtual_any.cpp#from_value template< typename T, typename = std::enable_if_t< @@ -205,6 +216,9 @@ class virtual_any { //! @tparam Class The type of the value to construct. //! @tparam T Types of the arguments to pass to the constructor. //! @param args Arguments to pass to the constructor of `Class`. + //! + //! @par Example + //! include:virtual_any.cpp#emplace template auto emplace(T&&... args) -> void { obj = Class(std::forward(args)...); @@ -491,6 +505,11 @@ struct select_overrider_virtual_type_aux< //! @param args Arguments to pass to the constructor of `Class`. //! @return A `virtual_any` containing a newly created //! `Class`. +//! +//! @par Example +//! include:virtual_any.cpp#make_any_virtual +//! +//! @see [Interoperation with Other Libraries](xref:ROOT:interop.adoc) template< class Class, class Any, class Registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY, typename... T> From 8bcb1a37b4c2783905356d75021e04c0f27e04e0 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 8 Aug 2026 13:37:58 -0400 Subject: [PATCH 16/64] alias use_std_any_types and use_boost_any_types The `any` headers aliased their wrapper type and their `make_` function but not the registration helper, so a program that imported `aliases` still had to spell `boost::openmethod::use_std_any_types` - as the doc example did. Alias them too, and let the example use `aliases` like the others. Co-Authored-By: Claude Fable 5 --- doc/modules/ROOT/examples/virtual_any.cpp | 4 ++-- include/boost/openmethod/interop/boost_any.hpp | 1 + include/boost/openmethod/interop/std_any.hpp | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/modules/ROOT/examples/virtual_any.cpp b/doc/modules/ROOT/examples/virtual_any.cpp index 83619e30..e94605ff 100644 --- a/doc/modules/ROOT/examples/virtual_any.cpp +++ b/doc/modules/ROOT/examples/virtual_any.cpp @@ -13,7 +13,7 @@ #include #include -using namespace boost::openmethod; +using namespace boost::openmethod::aliases; struct Dog { Dog(std::string name) : name(std::move(name)) {} @@ -47,7 +47,7 @@ BOOST_OPENMETHOD_OVERRIDE(poke, (const virtual_std_any& value), std::string) { #include int main() { - initialize(); + boost::openmethod::initialize(); // From an existing `any`: the v-table pointer is looked up from the type // of the value it contains. diff --git a/include/boost/openmethod/interop/boost_any.hpp b/include/boost/openmethod/interop/boost_any.hpp index ca633cdc..70125546 100644 --- a/include/boost/openmethod/interop/boost_any.hpp +++ b/include/boost/openmethod/interop/boost_any.hpp @@ -299,6 +299,7 @@ void final_virtual_ptr(boost::any&&) = delete; namespace aliases { using boost::openmethod::make_boost_any_virtual; +using boost::openmethod::use_boost_any_types; using boost::openmethod::virtual_boost_any; } // namespace aliases diff --git a/include/boost/openmethod/interop/std_any.hpp b/include/boost/openmethod/interop/std_any.hpp index 6269e3c3..623ec525 100644 --- a/include/boost/openmethod/interop/std_any.hpp +++ b/include/boost/openmethod/interop/std_any.hpp @@ -262,6 +262,7 @@ void final_virtual_ptr(std::any&&) = delete; namespace aliases { using boost::openmethod::make_std_any_virtual; +using boost::openmethod::use_std_any_types; using boost::openmethod::virtual_std_any; } // namespace aliases From 50d5ec91d6f4ed65fa79a9427e67e31e645bfec6 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 8 Aug 2026 13:55:54 -0400 Subject: [PATCH 17/64] support catch-all overriders on plain `any` virtual parameters `virtual_traits::cast` returns the wrapper unchanged when the overrider asks for it, which is how a catch-all overrider is written. The `std::any` and `boost::any` traits had no such case: they always `any_cast` to the overrider's parameter type, so an overrider taking `const std::any&` looked for an `any` stored inside the `any` and threw `bad_any_cast` at run time - the overrider was selected correctly, only the cast was wrong. Give the six `cast` overloads the same `if constexpr` as `virtual_any`, so a method with a `virtual_` parameter - or `&`, or `&&` - can have a catch-all, as one with a `virtual_any` parameter already could. The new tests also cover an `any` virtual parameter dispatching alongside a `virtual_ptr` in the same method, which had no coverage either. Co-Authored-By: Claude Fable 5 --- .../boost/openmethod/interop/boost_any.hpp | 36 +++++- include/boost/openmethod/interop/std_any.hpp | 42 ++++-- test/test_dispatch_boost_any.cpp | 115 +++++++++++++++++ test/test_dispatch_std_any.cpp | 122 +++++++++++++++++- 4 files changed, 296 insertions(+), 19 deletions(-) diff --git a/include/boost/openmethod/interop/boost_any.hpp b/include/boost/openmethod/interop/boost_any.hpp index 70125546..1452ba44 100644 --- a/include/boost/openmethod/interop/boost_any.hpp +++ b/include/boost/openmethod/interop/boost_any.hpp @@ -71,7 +71,9 @@ struct virtual_traits { //! Cast to a type. //! - //! Extracts the stored value using `boost::any_cast`. + //! If `U` is the `any` itself (by any reference category), returns + //! `arg` unchanged, which is how a catch-all overrider is written. + //! Otherwise, extracts the stored value using `boost::any_cast`. //! //! Since the `any` argument is const, `U` cannot be a mutable reference. //! `boost::any_cast` rewrites `U` to a const reference for a const `any`, @@ -87,7 +89,13 @@ struct virtual_traits { !std::is_reference_v || std::is_const_v>>> static auto cast(const boost::any& arg) -> decltype(auto) { - return boost::any_cast(arg); + if constexpr (std::is_same_v< + std::remove_cv_t>, + boost::any>) { + return (arg); + } else { + return boost::any_cast(arg); + } } }; @@ -134,7 +142,9 @@ struct virtual_traits { //! Cast to a type. //! - //! Extracts the stored value using `boost::any_cast`. Supports mutable + //! If `U` is the `any` itself (by any reference category), returns + //! `arg` unchanged, which is how a catch-all overrider is written. + //! Otherwise, extracts the stored value using `boost::any_cast`. Supports mutable //! references (e.g. `Dog&`) because the `any` argument is not const; //! modifications through the result are visible through the `any`. //! @@ -150,7 +160,13 @@ struct virtual_traits { template< typename U, typename = std::enable_if_t>> static auto cast(boost::any& arg) -> decltype(auto) { - return boost::any_cast(arg); + if constexpr (std::is_same_v< + std::remove_cv_t>, + boost::any>) { + return (arg); + } else { + return boost::any_cast(arg); + } } }; @@ -197,7 +213,9 @@ struct virtual_traits { //! Cast to a type. //! - //! Extracts the stored value using `boost::any_cast`. + //! If `U` is the `any` itself (by any reference category), returns + //! `arg` unchanged, which is how a catch-all overrider is written. + //! Otherwise, extracts the stored value using `boost::any_cast`. //! //! `U` cannot be a mutable lvalue reference: that would bind a reference //! to the value contained in a temporary. Boost.Any rejects it with a @@ -213,7 +231,13 @@ struct virtual_traits { !std::is_lvalue_reference_v || std::is_const_v>>> static auto cast(boost::any&& arg) -> decltype(auto) { - return boost::any_cast(std::move(arg)); + if constexpr (std::is_same_v< + std::remove_cv_t>, + boost::any>) { + return std::move(arg); + } else { + return boost::any_cast(std::move(arg)); + } } }; diff --git a/include/boost/openmethod/interop/std_any.hpp b/include/boost/openmethod/interop/std_any.hpp index 623ec525..6a13ca43 100644 --- a/include/boost/openmethod/interop/std_any.hpp +++ b/include/boost/openmethod/interop/std_any.hpp @@ -68,15 +68,23 @@ struct virtual_traits { //! Cast to a type. //! - //! Extracts the stored value using `std::any_cast`. Since the `any` - //! argument is const, `U` cannot be a mutable reference. + //! If `U` is the `any` itself (by any reference category), returns + //! `arg` unchanged, which is how a catch-all overrider is written. + //! Otherwise, extracts the stored value using `std::any_cast`. Since + //! the `any` argument is const, `U` cannot be a mutable reference. //! //! @tparam U The target type (e.g. `const Dog&`, `Dog`). //! @param arg A reference to a const `std::any` method argument. //! @return The value stored in `arg`, cast to `U`. template static auto cast(const std::any& arg) -> decltype(auto) { - return std::any_cast(arg); + if constexpr (std::is_same_v< + std::remove_cv_t>, + std::any>) { + return (arg); + } else { + return std::any_cast(arg); + } } }; @@ -120,16 +128,24 @@ struct virtual_traits { //! Cast to a type. //! - //! Extracts the stored value using `std::any_cast`. Supports mutable - //! references (e.g. `Dog&`) because the `any` argument is not const; - //! modifications through the result are visible through the `any`. + //! If `U` is the `any` itself, returns `arg` unchanged, which is how a + //! catch-all overrider is written. Otherwise, extracts the stored value + //! using `std::any_cast`. Supports mutable references (e.g. `Dog&`) + //! because the `any` argument is not const; modifications through the + //! result are visible through the `any`. //! //! @tparam U The target type (e.g. `Dog&`, `const Dog&`, `Dog`). //! @param arg A mutable reference to the `std::any` method argument. //! @return The value stored in `arg`, cast to `U`. template static auto cast(std::any& arg) -> decltype(auto) { - return std::any_cast(arg); + if constexpr (std::is_same_v< + std::remove_cv_t>, + std::any>) { + return (arg); + } else { + return std::any_cast(arg); + } } }; @@ -173,14 +189,22 @@ struct virtual_traits { //! Cast to a type. //! - //! Extracts the stored value using `std::any_cast`. + //! If `U` is the `any` itself, returns `arg` unchanged, which is how a + //! catch-all overrider is written. Otherwise, extracts the stored value + //! using `std::any_cast`. //! //! @tparam U The target type (e.g. `Dog&`, `const Dog&`, `Dog`). //! @param arg An rvalue reference to the `std::any` method argument. //! @return The value stored in `arg`, cast to `U`. template static auto cast(std::any&& arg) -> decltype(auto) { - return std::any_cast(std::move(arg)); + if constexpr (std::is_same_v< + std::remove_cv_t>, + std::any>) { + return std::move(arg); + } else { + return std::any_cast(std::move(arg)); + } } }; diff --git a/test/test_dispatch_boost_any.cpp b/test/test_dispatch_boost_any.cpp index bfd850db..7142f9bc 100644 --- a/test/test_dispatch_boost_any.cpp +++ b/test/test_dispatch_boost_any.cpp @@ -172,3 +172,118 @@ BOOST_AUTO_TEST_CASE(boost_any_by_xvalue_ref) { BOOST_TEST(boost::any_cast(answer) == 42); } } // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// catch-all overriders + +#define MAKE_CATCH_ALL_CLASSES() \ + struct Dog { \ + std::string name; \ + }; \ + \ + use_boost_any_types BOOST_OPENMETHOD_GENSYM; + +MAKE_CATCH_ALL_CLASSES(); + +// An overrider may take the `any` itself. Since every registered type +// derives from it, such an overrider is a catch-all, applying to any +// contained type that has no more specific overrider. + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const boost::any&), std::string) { + return "something else"; +} + +BOOST_OPENMETHOD(bump, (virtual_), std::string); + +using bump_method = + BOOST_OPENMETHOD_TYPE(bump, (virtual_), std::string); + +auto bump_dog(Dog& dog) -> std::string { + dog.name += " Jr."; + return dog.name + " the dog"; +} + +auto bump_any(boost::any&) -> std::string { + return "something else"; +} + +BOOST_OPENMETHOD_REGISTER(bump_method::override); +BOOST_OPENMETHOD_REGISTER(bump_method::override); + +BOOST_OPENMETHOD(steal, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(steal, (Dog && dog), std::string) { + Dog stolen(std::move(dog)); + return stolen.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(steal, (boost::any&&), std::string) { + return "something else"; +} + +BOOST_AUTO_TEST_CASE(boost_any_catch_all) { + initialize(trace()); + + boost::any spot(Dog{"Spot"}); + boost::any pi(3.14f); + + BOOST_TEST(name(spot) == "Spot the dog"); + BOOST_TEST(name(pi) == "something else"); + + BOOST_TEST(bump(spot) == "Spot Jr. the dog"); + BOOST_TEST(bump(pi) == "something else"); + + BOOST_TEST(steal(boost::any(Dog{"Fido"})) == "Fido the dog"); + BOOST_TEST(steal(std::move(pi)) == "something else"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// `any` and ordinary virtual parameters mixed in one method + +MAKE_CATCH_ALL_CLASSES(); + +struct Animal { + virtual ~Animal() { + } +}; + +struct Cat : Animal {}; + +BOOST_OPENMETHOD_CLASSES(Animal, Cat); + +BOOST_OPENMETHOD( + meet, (virtual_, virtual_ptr), + std::string); + +BOOST_OPENMETHOD_OVERRIDE( + meet, (const Dog& dog, virtual_ptr), std::string) { + return dog.name + " meets a cat"; +} + +BOOST_OPENMETHOD_OVERRIDE( + meet, (const boost::any&, virtual_ptr), std::string) { + return "someone meets an animal"; +} + +BOOST_AUTO_TEST_CASE(boost_any_mixed_with_virtual_ptr) { + initialize(trace()); + + boost::any spot(Dog{"Spot"}); + boost::any pi(3.14f); + Cat felix; + + BOOST_TEST(meet(spot, felix) == "Spot meets a cat"); + BOOST_TEST(meet(pi, felix) == "someone meets an animal"); +} +} // namespace BOOST_OPENMETHOD_GENSYM diff --git a/test/test_dispatch_std_any.cpp b/test/test_dispatch_std_any.cpp index 9c8e7025..61ee19f5 100644 --- a/test/test_dispatch_std_any.cpp +++ b/test/test_dispatch_std_any.cpp @@ -66,8 +66,8 @@ namespace BOOST_OPENMETHOD_GENSYM { // ----------------------------------------------------------------------------- // pass virtual args as std::any& (mutable ref) -static_assert(detail::has_vptr< - virtual_traits, type_id>); +static_assert( + detail::has_vptr, type_id>); MAKE_CLASSES(); @@ -129,8 +129,8 @@ namespace BOOST_OPENMETHOD_GENSYM { // ----------------------------------------------------------------------------- // pass virtual args as std::any&& (xvalue ref) -static_assert(detail::has_vptr< - virtual_traits, type_id>); +static_assert( + detail::has_vptr, type_id>); MAKE_CLASSES(); @@ -172,3 +172,117 @@ BOOST_AUTO_TEST_CASE(std_any_by_xvalue_ref) { BOOST_TEST(std::any_cast(answer) == 42); } } // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// catch-all overriders + +#define MAKE_CATCH_ALL_CLASSES() \ + struct Dog { \ + std::string name; \ + }; \ + \ + use_std_any_types BOOST_OPENMETHOD_GENSYM; + +MAKE_CATCH_ALL_CLASSES(); + +// An overrider may take the `any` itself. Since every registered type +// derives from it, such an overrider is a catch-all, applying to any +// contained type that has no more specific overrider. + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::any&), std::string) { + return "something else"; +} + +BOOST_OPENMETHOD(bump, (virtual_), std::string); + +using bump_method = + BOOST_OPENMETHOD_TYPE(bump, (virtual_), std::string); + +auto bump_dog(Dog& dog) -> std::string { + dog.name += " Jr."; + return dog.name + " the dog"; +} + +auto bump_any(std::any&) -> std::string { + return "something else"; +} + +BOOST_OPENMETHOD_REGISTER(bump_method::override); +BOOST_OPENMETHOD_REGISTER(bump_method::override); + +BOOST_OPENMETHOD(steal, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(steal, (Dog && dog), std::string) { + Dog stolen(std::move(dog)); + return stolen.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(steal, (std::any&&), std::string) { + return "something else"; +} + +BOOST_AUTO_TEST_CASE(std_any_catch_all) { + initialize(trace()); + + std::any spot(Dog{"Spot"}); + std::any pi(3.14f); + + BOOST_TEST(name(spot) == "Spot the dog"); + BOOST_TEST(name(pi) == "something else"); + + BOOST_TEST(bump(spot) == "Spot Jr. the dog"); + BOOST_TEST(bump(pi) == "something else"); + + BOOST_TEST(steal(std::any(Dog{"Fido"})) == "Fido the dog"); + BOOST_TEST(steal(std::move(pi)) == "something else"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// `any` and ordinary virtual parameters mixed in one method + +MAKE_CATCH_ALL_CLASSES(); + +struct Animal { + virtual ~Animal() { + } +}; + +struct Cat : Animal {}; + +BOOST_OPENMETHOD_CLASSES(Animal, Cat); + +BOOST_OPENMETHOD( + meet, (virtual_, virtual_ptr), std::string); + +BOOST_OPENMETHOD_OVERRIDE( + meet, (const Dog& dog, virtual_ptr), std::string) { + return dog.name + " meets a cat"; +} + +BOOST_OPENMETHOD_OVERRIDE( + meet, (const std::any&, virtual_ptr), std::string) { + return "someone meets an animal"; +} + +BOOST_AUTO_TEST_CASE(std_any_mixed_with_virtual_ptr) { + initialize(trace()); + + std::any spot(Dog{"Spot"}); + std::any pi(3.14f); + Cat felix; + + BOOST_TEST(meet(spot, felix) == "Spot meets a cat"); + BOOST_TEST(meet(pi, felix) == "someone meets an animal"); +} +} // namespace BOOST_OPENMETHOD_GENSYM From ff11651ef30435491cf27bee9ec5c1e331c48072 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 8 Aug 2026 13:56:05 -0400 Subject: [PATCH 18/64] doc: lead the Interoperation page with `virtual_`, not `virtual_any` The page opened on `virtual_std_any`, which put the wrapper - an optimization - before the plain thing it optimizes. Lead with `virtual_` instead: the example loses the construction dance and shrinks to a registration, four overriders and four calls. `virtual_std_any` becomes a section of its own, saying what it buys (the v-table lookup happens once, or not at all) and what limits it: the wrapper is not what an overrider receives, so an overrider cannot pass it on and save the lookup again. Only a catch-all overrider gets it. Also note that `any` virtual parameters and ordinary ones mix freely in a multi-method. The example and the reference snippets now use the classes and overriders of test/test_dispatch_std_any.cpp, so a reader moving between them meets one cast rather than two. `float` is registered without an overrider of its own, which is what the catch-all demonstrates - previously that role fell to `int`, which read as if it were registered for no reason. Co-Authored-By: Claude Fable 5 --- doc/modules/ROOT/examples/virtual_any.cpp | 60 +++++------ doc/modules/ROOT/pages/interop.adoc | 115 +++++++++++----------- doc/modules/ROOT/snippets/virtual_any.cpp | 97 +++++++++--------- 3 files changed, 127 insertions(+), 145 deletions(-) diff --git a/doc/modules/ROOT/examples/virtual_any.cpp b/doc/modules/ROOT/examples/virtual_any.cpp index e94605ff..f1a6adf8 100644 --- a/doc/modules/ROOT/examples/virtual_any.cpp +++ b/doc/modules/ROOT/examples/virtual_any.cpp @@ -13,61 +13,51 @@ #include #include -using namespace boost::openmethod::aliases; +using namespace boost::openmethod; struct Dog { - Dog(std::string name) : name(std::move(name)) {} - std::string name; -}; - -struct Cat { - Cat(std::string name) : name(std::move(name)) {} std::string name; }; // `std::any` becomes the common base of the types it may contain. -BOOST_OPENMETHOD_REGISTER(use_std_any_types); +BOOST_OPENMETHOD_REGISTER(use_std_any_types); -BOOST_OPENMETHOD(poke, (const virtual_std_any&), std::string); +BOOST_OPENMETHOD(name, (virtual_), std::string); // An overrider takes the contained value... -BOOST_OPENMETHOD_OVERRIDE(poke, (const Dog& dog), std::string) { - return dog.name + " barks"; +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; } -BOOST_OPENMETHOD_OVERRIDE(poke, (const Cat& cat), std::string) { - return cat.name + " hisses"; +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { + return name; } -// ...or the `virtual_any` itself, which makes it a catch-all. -BOOST_OPENMETHOD_OVERRIDE(poke, (const virtual_std_any& value), std::string) { - return value.get().has_value() ? "it does nothing" : "nothing happens"; +BOOST_OPENMETHOD_OVERRIDE(name, (const int& value), std::string) { + return std::to_string(value) + " the integer"; +} + +// ...or the `any` itself, which makes it a catch-all. +BOOST_OPENMETHOD_OVERRIDE(name, (const std::any&), std::string) { + return "something else"; } #include int main() { - boost::openmethod::initialize(); - - // From an existing `any`: the v-table pointer is looked up from the type - // of the value it contains. - std::any snoopy_any = Dog("Snoopy"); - virtual_std_any snoopy = snoopy_any; - - // From a value: the type is known at compile time, so the v-table pointer - // is read from a static variable, with no lookup. - virtual_std_any felix = Cat("Felix"); + initialize(); - // Same, constructing the value in place. - auto hector = make_std_any_virtual("Hector"); + std::any spot = Dog{"Spot"}; + std::any felix = std::string("Felix the cat"); + std::any answer = 42; + std::any pi = 3.14f; - std::cout << poke(snoopy) << "\n"; // Snoopy barks - std::cout << poke(felix) << "\n"; // Felix hisses - std::cout << poke(hector) << "\n"; // Hector barks + std::cout << name(spot) << "\n"; // Spot the dog + std::cout << name(felix) << "\n"; // Felix the cat + std::cout << name(answer) << "\n"; // 42 the integer - // `int` is registered, but has no overrider of its own: the catch-all - // applies. The value converts to a temporary `virtual_std_any` at the - // call site. - std::cout << poke(42) << "\n"; // it does nothing + // `float` is registered, but has no overrider of its own, so the + // catch-all applies. + std::cout << name(pi) << "\n"; // something else } // end::content[] diff --git a/doc/modules/ROOT/pages/interop.adoc b/doc/modules/ROOT/pages/interop.adoc index 25f36bfa..adb907d8 100644 --- a/doc/modules/ROOT/pages/interop.adoc +++ b/doc/modules/ROOT/pages/interop.adoc @@ -26,15 +26,11 @@ that is not registered cannot be dispatched on; a call with such a value in the `any` is a cpp:missing_class[] error - see xref:error_handling.adoc[Error Handling]. -cpp:virtual_std_any[] - an alias for `virtual_any` - is to an `any` -what cpp:virtual_ptr[] is to a pointer: it bundles the `any` with a pointer to -the v-table for the value it contains, so a call does not have to look that -v-table up. Unlike `virtual_ptr`, it _owns_ the object: the `any` is held by -value. - -Overriders receive the _contained_ value, by a reference of a compatible -category - not the wrapper. An overrider may also take the `virtual_std_any` -itself, unchanged; since every registered type derives from `std::any`, such an +The `any` is then passed like any other virtual argument that is not a +`virtual_ptr`: wrapped in `virtual_`, as described in +xref:virtual_ptr_alt.adoc[Alternatives to virtual_ptr]. Overriders receive the +_contained_ value, by a reference of a compatible category. An overrider may +also take the `any` itself; since every registered type derives from it, such an overrider is a catch-all, applying to any contained type that has no more specific overrider: @@ -43,54 +39,54 @@ specific overrider: include::example$virtual_any.cpp[tag=content] ---- -#### Where the v-table pointer comes from +#### Mixing with ordinary virtual parameters -A `virtual_any` acquires its v-table pointer once, when it is created, and -maintains it across assignment and `emplace`. There are two ways it can do so: +An `any` virtual parameter is an ordinary virtual parameter that happens to +resolve through the contained type, so it composes with the others without +restriction. A multi-method can dispatch on an `any` and on a `virtual_ptr`, or +a plain reference, in the same call: -- From an existing `any`, the contained type is known only at run time, so the -v-table pointer is looked up in a hash table, keyed on the type of the contained -value. - -- From a value, or from cpp:make_std_any_virtual[], or from `emplace`, the -contained type is known at compile time, so the v-table pointer is simply read -from a static variable - no lookup at all. +```c++ +BOOST_OPENMETHOD( + meet, (virtual_, virtual_ptr), std::string); -The second form is the one to prefer where we have the choice. +BOOST_OPENMETHOD_OVERRIDE( + meet, (const Dog& dog, virtual_ptr), std::string) { + return dog.name + " meets a cat"; +} +``` #### Reference categories -A `virtual_std_any` method parameter must be a reference - passing it by value -would copy the `any`, and the value inside it, on every call. All three -reference categories are supported, and they determine what the overriders may -take: +All three reference categories are supported, and they determine what the +overriders may take: [cols="1,2"] |=== | Method parameter | Overrider parameter -| `const virtual_std_any&` +| `virtual_` | `const Dog&`, `Dog` -| `virtual_std_any&` +| `virtual_` | `Dog&`, `const Dog&`, `Dog` -| `virtual_std_any&&` +| `virtual_` | `Dog&&`, `const Dog&`, `Dog` |=== -A value converts implicitly to a `virtual_std_any`, so `poke(42)` in the example -above creates a temporary at the call site. That temporary binds to `const -virtual_std_any&` and to `virtual_std_any&&`, but nothing binds to a mutable -lvalue reference. Consequently -xref:reference:BOOST_OPENMETHOD_OVERRIDE.adoc[BOOST_OPENMETHOD_OVERRIDE], which -locates the method by checking that the overrider's parameters can be passed to -the method's forwarder, cannot be used with a `virtual_std_any&` method. Such -overriders are registered with the core API instead - the primitive the macro -itself expands to: +The mutable lvalue reference is the awkward one. +xref:reference:BOOST_OPENMETHOD_OVERRIDE.adoc[BOOST_OPENMETHOD_OVERRIDE] locates +the method by checking that the overrider's parameters can be passed to the +method's forwarder, and `Dog&` does not convert to `std::any&`. A temporary +`std::any` binds to `const std::any&` and to `std::any&&`, which is why the +other two categories can use the macro; nothing binds to a mutable lvalue +reference. Those overriders are registered with the core API instead - the +primitive the macro itself expands to: ```c++ -using bump_method = BOOST_OPENMETHOD_TYPE(bump, (virtual_std_any&), std::string); +using bump_method = + BOOST_OPENMETHOD_TYPE(bump, (virtual_), std::string); auto bump_dog(Dog& dog) -> std::string { dog.name += " Jr."; @@ -100,33 +96,34 @@ auto bump_dog(Dog& dog) -> std::string { BOOST_OPENMETHOD_REGISTER(bump_method::override); ``` -#### Dispatching on a plain `any` +#### `virtual_std_any` -`virtual_std_any` is not mandatory. A `std::any` can also be used directly in a -virtual parameter, wrapped in `virtual_`, as described in -xref:virtual_ptr_alt.adoc[Alternatives to virtual_ptr]: +Every call above looks the v-table up in a hash table, keyed on the type the +`any` contains. cpp:virtual_std_any[] - an alias for `virtual_any` - +removes that cost: it bundles an `any` with the v-table pointer for the value +inside it, acquiring it once, on construction, and maintaining it across +assignment and `emplace`. It is to an `any` what cpp:virtual_ptr[] is to a +pointer, except that it _owns_ the object: the `any` is held by value. -```c++ -BOOST_OPENMETHOD(poke, (virtual_), std::string); -``` - -The overriders are written exactly as before, and the same `use_std_any_types` -registration applies. The difference is where the v-table pointer comes from: -there is nowhere to cache it, so every call performs the hash table lookup. +The pointer comes from a lookup when the `virtual_std_any` is built from an +existing `any`, and from a static variable - no lookup at all - when it is built +from a value, or by cpp:make_std_any_virtual[], or by `emplace`, since the type +is then known at compile time. -Which to use: +That makes it worthwhile when the same value is dispatched on repeatedly. Its +usefulness is limited, though, by the fact that the wrapper is not what an +overrider receives: an overrider takes the contained value, as before, so it +cannot pass the `virtual_std_any` on to another method and save the lookup +there. Only a catch-all overrider, which takes `const virtual_std_any&`, gets +it. -- `virtual_`, when the `any` comes from elsewhere - an existing -API, a container of `std::any` - and is dispatched on once. It adds nothing to -the `any`, and requires no change to the code that produces it. - -- `virtual_std_any`, when the same value is dispatched on repeatedly, or when we -create it ourselves and its type is statically known. The lookup then happens -once, or not at all. +A `virtual_std_any` method parameter must be a reference - passing it by value +would copy the `any`, and the value inside it, on every call. The three +categories, and the limitation on the mutable one, are as above. -For the same reason, cpp:final_virtual_ptr[] is _deleted_ for `std::any`: it -would silently produce the v-table of the `any` root class rather than the one -for the contained value. +For the same reason that a `virtual_std_any` caches what a plain `any` does not, +cpp:final_virtual_ptr[] is _deleted_ for `std::any`: it would silently produce +the v-table of the `any` root class rather than the one for the contained value. #### `boost::any` diff --git a/doc/modules/ROOT/snippets/virtual_any.cpp b/doc/modules/ROOT/snippets/virtual_any.cpp index 7a093f46..ad8e3713 100644 --- a/doc/modules/ROOT/snippets/virtual_any.cpp +++ b/doc/modules/ROOT/snippets/virtual_any.cpp @@ -23,38 +23,32 @@ namespace std_any { // tag::classes[] struct Dog { - Dog(std::string name) : name(std::move(name)) { - } - - std::string name; -}; - -struct Cat { - Cat(std::string name) : name(std::move(name)) { - } - std::string name; }; // `std::any` becomes the common base of the types it may contain. -BOOST_OPENMETHOD_REGISTER(use_std_any_types); +BOOST_OPENMETHOD_REGISTER(use_std_any_types); // end::classes[] // tag::method[] -BOOST_OPENMETHOD(poke, (const virtual_std_any&), std::string); +BOOST_OPENMETHOD(name, (const virtual_std_any&), std::string); // An overrider takes the contained value... -BOOST_OPENMETHOD_OVERRIDE(poke, (const Dog& dog), std::string) { - return dog.name + " barks"; +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; } -BOOST_OPENMETHOD_OVERRIDE(poke, (const Cat& cat), std::string) { - return cat.name + " hisses"; +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { + return name; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const int& value), std::string) { + return std::to_string(value) + " the integer"; } // ...or the `virtual_any` itself, which makes it a catch-all. -BOOST_OPENMETHOD_OVERRIDE(poke, (const virtual_std_any& value), std::string) { - return value.get().has_value() ? "it does nothing" : "nothing happens"; +BOOST_OPENMETHOD_OVERRIDE(name, (const virtual_std_any& value), std::string) { + return value.get().has_value() ? "something else" : "nothing"; } // end::method[] @@ -64,20 +58,21 @@ namespace boost_any { // tag::boost_classes[] struct Dog { - Dog(std::string name) : name(std::move(name)) { - } - std::string name; }; // `boost::any` is a root class of its own, distinct from the one used for // `std::any`, so both may be used in the same program and registry. -BOOST_OPENMETHOD_REGISTER(use_boost_any_types); +BOOST_OPENMETHOD_REGISTER(use_boost_any_types); + +BOOST_OPENMETHOD(name, (const virtual_boost_any&), std::string); -BOOST_OPENMETHOD(poke, (const virtual_boost_any&), std::string); +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} -BOOST_OPENMETHOD_OVERRIDE(poke, (const Dog& dog), std::string) { - return dog.name + " barks"; +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { + return name; } // end::boost_classes[] @@ -92,85 +87,85 @@ BOOST_AUTO_TEST_CASE(std_any_examples) { capture_cout cout; // tag::dispatch[] - virtual_std_any snoopy = Dog("Snoopy"); + virtual_std_any spot = Dog{"Spot"}; - std::cout << poke(snoopy) << "\n"; // Snoopy barks + std::cout << name(spot) << "\n"; // Spot the dog - // `int` is registered, but has no overrider of its own, so the + // `float` is registered, but has no overrider of its own, so the // catch-all applies. The value converts to a temporary // `virtual_std_any` at the call site. - std::cout << poke(42) << "\n"; // it does nothing + std::cout << name(3.14f) << "\n"; // something else // end::dispatch[] - BOOST_TEST(cout.str() == "Snoopy barks\nit does nothing\n"); + BOOST_TEST(cout.str() == "Spot the dog\nsomething else\n"); } { capture_cout cout; // tag::from_any[] - std::any snoopy_any = Dog("Snoopy"); + std::any spot_any = Dog{"Spot"}; // the v-table pointer is looked up from the type of the value the // `any` contains - virtual_std_any snoopy = snoopy_any; + virtual_std_any spot = spot_any; - std::cout << poke(snoopy) << "\n"; // Snoopy barks + std::cout << name(spot) << "\n"; // Spot the dog // end::from_any[] - BOOST_TEST(cout.str() == "Snoopy barks\n"); + BOOST_TEST(cout.str() == "Spot the dog\n"); } { capture_cout cout; // tag::from_value[] - // `Cat` is known at compile time, so the v-table pointer is read + // the type is known at compile time, so the v-table pointer is read // from a static variable - there is no lookup - virtual_std_any felix = Cat("Felix"); + virtual_std_any answer = 42; - std::cout << poke(felix) << "\n"; // Felix hisses + std::cout << name(answer) << "\n"; // 42 the integer // end::from_value[] - BOOST_TEST(cout.str() == "Felix hisses\n"); + BOOST_TEST(cout.str() == "42 the integer\n"); } { capture_cout cout; // tag::emplace[] - virtual_std_any animal; + virtual_std_any value; - animal.emplace("Felix"); + value.emplace("Felix the cat"); - std::cout << poke(animal) << "\n"; // Felix hisses + std::cout << name(value) << "\n"; // Felix the cat // end::emplace[] - BOOST_TEST(cout.str() == "Felix hisses\n"); + BOOST_TEST(cout.str() == "Felix the cat\n"); } { capture_cout cout; // tag::make_any_virtual[] - auto felix = make_any_virtual("Felix"); + auto felix = make_any_virtual("Felix the cat"); - std::cout << poke(felix) << "\n"; // Felix hisses + std::cout << name(felix) << "\n"; // Felix the cat // end::make_any_virtual[] - BOOST_TEST(cout.str() == "Felix hisses\n"); + BOOST_TEST(cout.str() == "Felix the cat\n"); } { capture_cout cout; // tag::make_std_any_virtual[] - auto snoopy = make_std_any_virtual("Snoopy"); + auto felix = make_std_any_virtual("Felix the cat"); - std::cout << poke(snoopy) << "\n"; // Snoopy barks + std::cout << name(felix) << "\n"; // Felix the cat // end::make_std_any_virtual[] - BOOST_TEST(cout.str() == "Snoopy barks\n"); + BOOST_TEST(cout.str() == "Felix the cat\n"); } } @@ -183,11 +178,11 @@ BOOST_AUTO_TEST_CASE(boost_any_examples) { capture_cout cout; // tag::boost_dispatch[] - auto snoopy = make_boost_any_virtual("Snoopy"); + auto felix = make_boost_any_virtual("Felix the cat"); - std::cout << poke(snoopy) << "\n"; // Snoopy barks + std::cout << name(felix) << "\n"; // Felix the cat // end::boost_dispatch[] - BOOST_TEST(cout.str() == "Snoopy barks\n"); + BOOST_TEST(cout.str() == "Felix the cat\n"); } } From acdd8e0324e857bc06c45da985879fe1a7bfe290 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 8 Aug 2026 15:10:37 -0400 Subject: [PATCH 19/64] test: make the virtual_any-by-value compile-fail test actually fail on MSVC `BOOST_OPENMETHOD` only declares a forwarder function template; it does not instantiate `method<...>`. The guard against a by-value `virtual_any` lives in the `method` class body, so GCC and Clang - which instantiate the class at the declaration - diagnosed it, while MSVC waited until the method was used. The test never used it, so it compiled clean and the `*fail` target failed on both Windows Drone stages. Call the method in `main()`, like every other compile-fail test. Co-Authored-By: Claude Opus 5 --- test/compile_fail_virtual_any_by_value.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/compile_fail_virtual_any_by_value.cpp b/test/compile_fail_virtual_any_by_value.cpp index 97ea54ba..22afdf0b 100644 --- a/test/compile_fail_virtual_any_by_value.cpp +++ b/test/compile_fail_virtual_any_by_value.cpp @@ -22,5 +22,6 @@ BOOST_OPENMETHOD_REGISTER(use_std_any_types); BOOST_OPENMETHOD(name, (virtual_std_any), std::string); int main() { - return 0; + auto dog = make_std_any_virtual(Dog{"Snoopy"}); + return name(dog).size(); } From e946dd26a41432b1b19b8fa6ea1d95a45b3bc53c Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 8 Aug 2026 15:38:22 -0400 Subject: [PATCH 20/64] doc: fix the broken `any` header links on the Headers page The virtual_any, std_any and boost_any entries spelled the source link as `{{BASE_URL}}/...`, which Antora does not substitute, so the three links rendered with the placeholder as literal text. Use `{base-url}`, the attribute defined in antora.yml and used by the other 17 header links. Co-Authored-By: Claude Opus 5 --- doc/modules/ROOT/pages/ref_headers.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/modules/ROOT/pages/ref_headers.adoc b/doc/modules/ROOT/pages/ref_headers.adoc index 7c30c404..fd6df86e 100644 --- a/doc/modules/ROOT/pages/ref_headers.adoc +++ b/doc/modules/ROOT/pages/ref_headers.adoc @@ -72,7 +72,7 @@ Provides a `virtual_traits` specialization that makes it possible to use a `boost::intrusive_ptr` in place of a raw pointer or reference in virtual parameters. [#virtual_any] -### link:{{BASE_URL}}/include/boost/openmethod/interop/virtual_any.hpp[] +### link:{base-url}/include/boost/openmethod/interop/virtual_any.hpp[] Provides `virtual_any`, a wide `any` that combines an `any`, held by value, with a pointer to the v-table for the contained value - like `virtual_ptr` @@ -86,7 +86,7 @@ with an `any`-like interface, given `virtual_traits` specializations for its reference types. [#std_any] -### link:{{BASE_URL}}/include/boost/openmethod/interop/std_any.hpp[] +### link:{base-url}/include/boost/openmethod/interop/std_any.hpp[] Provides `virtual_traits` specializations that make it possible to use a `std::any` - by const reference, by mutable reference, or by rvalue reference - in virtual @@ -98,7 +98,7 @@ parameters. Dispatch is on the type of the contained value. Also provides use the v-table of the `any` root class instead of the contained value's. [#boost_any] -### link:{{BASE_URL}}/include/boost/openmethod/interop/boost_any.hpp[] +### link:{base-url}/include/boost/openmethod/interop/boost_any.hpp[] Provides `virtual_traits` specializations that make it possible to use a `boost::any` - by const reference, by mutable reference, or by rvalue reference - in virtual From f1d3cd82ec98cfabeb136717cd630b273cd75f32 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 8 Aug 2026 15:46:14 -0400 Subject: [PATCH 21/64] doc: narrow the Interoperation page to `any` Every section of the page is about dispatching on the type contained in an `any`, but the title, the file name and the opening paragraph all promised a broader page. Rename interop.adoc to interop_any.adoc, retitle it "Interoperation with `any`", and drop the intro's "or a pointer class of their own" clause, which anticipated content the page does not have. Update the nav entry, the page anchor, and the eight `@see` links in the interop headers. Co-Authored-By: Claude Opus 5 --- doc/modules/ROOT/nav.adoc | 2 +- .../ROOT/pages/{interop.adoc => interop_any.adoc} | 11 +++++------ include/boost/openmethod/interop/boost_any.hpp | 6 +++--- include/boost/openmethod/interop/std_any.hpp | 6 +++--- include/boost/openmethod/interop/virtual_any.hpp | 4 ++-- 5 files changed, 14 insertions(+), 15 deletions(-) rename doc/modules/ROOT/pages/{interop.adoc => interop_any.adoc} (94%) diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index 5f6dc368..6edd8231 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -13,7 +13,7 @@ ** xref:custom_rtti.adoc[Custom RTTI] ** xref:error_handling.adoc[Error Handling] ** xref:virtual_ptr_alt.adoc[Virtual Pointer Alternatives] -** xref:interop.adoc[Interoperation with Other Libraries] +** xref:interop_any.adoc[Interoperation with `any`] ** xref:shared_libraries.adoc[Shared Libraries] * xref:reference:index.adoc[Reference] ** xref:ref_headers.adoc[Headers] diff --git a/doc/modules/ROOT/pages/interop.adoc b/doc/modules/ROOT/pages/interop_any.adoc similarity index 94% rename from doc/modules/ROOT/pages/interop.adoc rename to doc/modules/ROOT/pages/interop_any.adoc index adb907d8..d91e7c86 100644 --- a/doc/modules/ROOT/pages/interop.adoc +++ b/doc/modules/ROOT/pages/interop_any.adoc @@ -1,11 +1,10 @@ -[#interop] -## Interoperation with Other Libraries +[#interop_any] +## Interoperation with `any` -Some libraries hand us a value whose type is not visible in the static type of -the variable that holds it - a type-erased container, or a pointer class of -their own. This section covers the constructs that let a method look through -such a wrapper and dispatch on what is really inside. +A value held in an `any` has a type that is not visible in the static type of +the variable holding the `any`. This section covers the constructs that let a +method look through the wrapper and dispatch on what is really inside. ### `any` diff --git a/include/boost/openmethod/interop/boost_any.hpp b/include/boost/openmethod/interop/boost_any.hpp index 1452ba44..6bab0463 100644 --- a/include/boost/openmethod/interop/boost_any.hpp +++ b/include/boost/openmethod/interop/boost_any.hpp @@ -258,7 +258,7 @@ struct virtual_traits { //! @par Example //! include:virtual_any.cpp#boost_classes;boost_dispatch //! -//! @see [Interoperation with Other Libraries](xref:ROOT:interop.adoc) +//! @see [Interoperation with `any`](xref:ROOT:interop_any.adoc) template struct use_boost_any_types : detail::use_class_aux< @@ -272,7 +272,7 @@ struct use_boost_any_types //! //! With another registry, use `virtual_any` directly. //! -//! @see [Interoperation with Other Libraries](xref:ROOT:interop.adoc) +//! @see [Interoperation with `any`](xref:ROOT:interop_any.adoc) using virtual_boost_any = virtual_any; //! Create a new object and return a `virtual_boost_any` containing it. @@ -293,7 +293,7 @@ using virtual_boost_any = virtual_any; //! @par Example //! include:virtual_any.cpp#boost_dispatch //! -//! @see [Interoperation with Other Libraries](xref:ROOT:interop.adoc) +//! @see [Interoperation with `any`](xref:ROOT:interop_any.adoc) template< class Class, class Registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY, typename... T> diff --git a/include/boost/openmethod/interop/std_any.hpp b/include/boost/openmethod/interop/std_any.hpp index 6a13ca43..73f05f71 100644 --- a/include/boost/openmethod/interop/std_any.hpp +++ b/include/boost/openmethod/interop/std_any.hpp @@ -221,7 +221,7 @@ struct virtual_traits { //! @par Example //! include:virtual_any.cpp#classes //! -//! @see [Interoperation with Other Libraries](xref:ROOT:interop.adoc) +//! @see [Interoperation with `any`](xref:ROOT:interop_any.adoc) template struct use_std_any_types : detail::use_class_aux< @@ -235,7 +235,7 @@ struct use_std_any_types //! //! With another registry, use `virtual_any` directly. //! -//! @see [Interoperation with Other Libraries](xref:ROOT:interop.adoc) +//! @see [Interoperation with `any`](xref:ROOT:interop_any.adoc) using virtual_std_any = virtual_any; //! Create a new object and return a `virtual_std_any` containing it. @@ -256,7 +256,7 @@ using virtual_std_any = virtual_any; //! @par Example //! include:virtual_any.cpp#make_std_any_virtual //! -//! @see [Interoperation with Other Libraries](xref:ROOT:interop.adoc) +//! @see [Interoperation with `any`](xref:ROOT:interop_any.adoc) template< class Class, class Registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY, typename... T> diff --git a/include/boost/openmethod/interop/virtual_any.hpp b/include/boost/openmethod/interop/virtual_any.hpp index 485f569d..6a9e1171 100644 --- a/include/boost/openmethod/interop/virtual_any.hpp +++ b/include/boost/openmethod/interop/virtual_any.hpp @@ -61,7 +61,7 @@ struct is_virtual_any_aux> : std::true_type {}; //! @par Example //! include:virtual_any.cpp#classes;method;dispatch //! -//! @see [Interoperation with Other Libraries](xref:ROOT:interop.adoc) +//! @see [Interoperation with `any`](xref:ROOT:interop_any.adoc) template class virtual_any { static constexpr bool use_indirect_vptrs = Registry::has_indirect_vptr; @@ -509,7 +509,7 @@ struct select_overrider_virtual_type_aux< //! @par Example //! include:virtual_any.cpp#make_any_virtual //! -//! @see [Interoperation with Other Libraries](xref:ROOT:interop.adoc) +//! @see [Interoperation with `any`](xref:ROOT:interop_any.adoc) template< class Class, class Any, class Registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY, typename... T> From 96639749adb77407f467d0308e98132e0dcc8e4e Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 8 Aug 2026 16:13:54 -0400 Subject: [PATCH 22/64] doc: an Interoperation with Boost.TypeErasure page, and reference examples Co-Authored-By: Claude Fable 5 --- doc/modules/ROOT/examples/type_erasure.cpp | 67 +++++++++ .../ROOT/examples/type_erasure_ref.cpp | 59 ++++++++ doc/modules/ROOT/nav.adoc | 1 + doc/modules/ROOT/pages/interop_any.adoc | 2 + .../ROOT/pages/interop_type_erasure.adoc | 131 ++++++++++++++++++ doc/modules/ROOT/snippets/type_erasure.cpp | 78 +++++++++++ .../openmethod/interop/boost_type_erasure.hpp | 15 ++ 7 files changed, 353 insertions(+) create mode 100644 doc/modules/ROOT/examples/type_erasure.cpp create mode 100644 doc/modules/ROOT/examples/type_erasure_ref.cpp create mode 100644 doc/modules/ROOT/pages/interop_type_erasure.adoc create mode 100644 doc/modules/ROOT/snippets/type_erasure.cpp diff --git a/doc/modules/ROOT/examples/type_erasure.cpp b/doc/modules/ROOT/examples/type_erasure.cpp new file mode 100644 index 00000000..21920eb4 --- /dev/null +++ b/doc/modules/ROOT/examples/type_erasure.cpp @@ -0,0 +1,67 @@ +// Copyright (c) 2018-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) + +// clang-format off + +// tag::content[] +#include +#include + +#include +#include +#include +#include + +#include +#include + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +// `relaxed` implies `typeid_<>`, which dispatch relies on. +using Concept = boost::mpl::vector, te::relaxed>; +using erased = te::any; + +struct Dog { + std::string name; +}; + +// The owning flavor, `any`, becomes the common base of the types +// the `any` may bind. +BOOST_OPENMETHOD_REGISTER(use_type_erasure_types); + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +// An overrider takes the bound value... +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { + return name; +} + +// ...or the `any` itself, which makes it a catch-all. +BOOST_OPENMETHOD_OVERRIDE(name, (const erased& value), std::string) { + return te::is_empty(value) ? "nothing" : "something else"; +} + +#include + +int main() { + initialize(); + + const erased spot(Dog{"Spot"}); + const erased felix(std::string("Felix the cat")); + const erased answer(42); + + std::cout << name(spot) << "\n"; // Spot the dog + std::cout << name(felix) << "\n"; // Felix the cat + + // `int` is registered, but has no overrider of its own, so the + // catch-all applies. + std::cout << name(answer) << "\n"; // something else +} +// end::content[] diff --git a/doc/modules/ROOT/examples/type_erasure_ref.cpp b/doc/modules/ROOT/examples/type_erasure_ref.cpp new file mode 100644 index 00000000..13daf9f2 --- /dev/null +++ b/doc/modules/ROOT/examples/type_erasure_ref.cpp @@ -0,0 +1,59 @@ +// Copyright (c) 2018-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) + +// clang-format off + +// tag::content[] +#include +#include + +#include +#include +#include + +#include +#include + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +using Concept = boost::mpl::vector, te::relaxed>; +using erased_ref = te::any; + +struct Dog { + std::string name; +}; + +BOOST_OPENMETHOD_REGISTER(use_type_erasure_types); + +// The reference-wrapper flavor is a cheap handle; it is passed by value. +BOOST_OPENMETHOD(poke, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(poke, (Dog& dog), std::string) { + dog.name += "!"; + return dog.name; +} + +BOOST_OPENMETHOD_OVERRIDE(poke, (int& value), std::string) { + ++value; + return "poked"; +} + +#include + +int main() { + initialize(); + + Dog snoopy{"Snoopy"}; + int count = 41; + + // mutations reach the referents + std::cout << poke(erased_ref(snoopy)) << "\n"; // Snoopy! + std::cout << snoopy.name << "\n"; // Snoopy! + + std::cout << poke(erased_ref(count)) << "\n"; // poked + std::cout << count << "\n"; // 42 +} +// end::content[] diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index 6edd8231..859605d7 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -14,6 +14,7 @@ ** xref:error_handling.adoc[Error Handling] ** xref:virtual_ptr_alt.adoc[Virtual Pointer Alternatives] ** xref:interop_any.adoc[Interoperation with `any`] +** xref:interop_type_erasure.adoc[Interoperation with Boost.TypeErasure] ** xref:shared_libraries.adoc[Shared Libraries] * xref:reference:index.adoc[Reference] ** xref:ref_headers.adoc[Headers] diff --git a/doc/modules/ROOT/pages/interop_any.adoc b/doc/modules/ROOT/pages/interop_any.adoc index d91e7c86..0f5d2149 100644 --- a/doc/modules/ROOT/pages/interop_any.adoc +++ b/doc/modules/ROOT/pages/interop_any.adoc @@ -135,3 +135,5 @@ registry. cpp:virtual_any[] itself is generic: it can serve any type with an `any`-like interface, given cpp:virtual_traits[] specializations for its reference types. +Boost.TypeErasure's `any` is supported on the same model - see +xref:interop_type_erasure.adoc[Interoperation with Boost.TypeErasure]. diff --git a/doc/modules/ROOT/pages/interop_type_erasure.adoc b/doc/modules/ROOT/pages/interop_type_erasure.adoc new file mode 100644 index 00000000..957406ff --- /dev/null +++ b/doc/modules/ROOT/pages/interop_type_erasure.adoc @@ -0,0 +1,131 @@ + +[#interop_type_erasure] +## Interoperation with Boost.TypeErasure + +link:https://www.boost.org/doc/libs/release/doc/html/boost_typeerasure.html[Boost.TypeErasure]'s +`any` erases the type of its content, like `std::any`, but couples that with a +Concept: a compile-time list of the operations the content must support. +OpenMethod can dispatch on the type bound to such an `any`, in the same manner +as for a plain `any` - see xref:interop_any.adoc[Interoperation with `any`]. + +Support is provided by ``. It +is not included by ``, so it must be included explicitly. + +### `type_erasure::any` + +Dispatch resolves on the type returned by `boost::type_erasure::typeid_of`, so +the only requirement placed on the Concept is that it contain +`boost::type_erasure::typeid_<>` - which `relaxed` already implies. + +The types the `any` may bind have to be registered. +cpp:use_type_erasure_types[] does that, registering the owning flavor - +`any` - as a class, and each of the types as a class derived from it. +Each Concept gets a root of its own, so `any`s with different Concepts - and +the plain `any`s - can coexist in the same registry. A type that is not +registered cannot be dispatched on; a call with such a value bound to the +`any` is a cpp:missing_class[] error - see +xref:error_handling.adoc[Error Handling]. + +The `any` is passed wrapped in `virtual_`, and overriders receive the _bound_ +value - or, for a catch-all overrider, the `any` itself: + +[source,c++] +---- +include::example$type_erasure.cpp[tag=content] +---- + +#### Reference categories + +The owning flavor is passed by reference, in any of the three categories - +passing it by value would copy the bound value on every call, and is rejected +at compile time. The category determines what the overriders may take: + +[cols="1,2"] +|=== +| Method parameter | Overrider parameter + +| `virtual_&>` +| `const Dog&`, `Dog` + +| `virtual_&>` +| `Dog&`, `const Dog&`, `Dog` + +| `virtual_&&>` +| `Dog&&`, `const Dog&`, `Dog` +|=== + +The mutable lvalue reference has the same limitation as `virtual_`: +xref:reference:BOOST_OPENMETHOD_OVERRIDE.adoc[BOOST_OPENMETHOD_OVERRIDE] +cannot locate the method, because nothing binds a temporary `any` to a mutable +lvalue reference - see the +xref:interop_any.adoc#interop_any[explanation there]. Those overriders are +registered with the core API instead: + +```c++ +using bump_method = + BOOST_OPENMETHOD_TYPE(bump, (virtual_&>), std::string); + +auto bump_dog(Dog& dog) -> std::string { + dog.name += " Jr."; + return dog.name; +} + +BOOST_OPENMETHOD_REGISTER(bump_method::override); +``` + +`boost::type_erasure::any_cast` has no rvalue overload, so, in the rvalue +case, moving the value out of the `any` is performed by the interop code +itself: an overrider taking `Dog&&` receives the bound value ready to be moved +from, and the `any` still owns the moved-from object afterwards. + +#### The reference-wrapper flavors + +Boost.TypeErasure also has non-owning flavors, `any` and +`any`, which hold a _reference_ to a value stored +elsewhere. They are cheap, two-word handles, and, unlike the owning flavor, +they are passed by value - the idiomatic way to use them as parameters. +Modifications made through a mutable-reference wrapper reach the referent: + +[source,c++] +---- +include::example$type_erasure_ref.cpp[tag=content] +---- + +Dispatch is on the type _bound at construction_ of the wrapper - never on the +C++ RTTI dynamic type of the referent. The rvalue-reference flavor +(`any`), and placeholders other than `_self`, are not +supported. + +#### `virtual_any` + +Every call above looks the v-table up in a hash table, keyed on the type the +`any` binds. cpp:virtual_any[] works for a `type_erasure::any` exactly as it +does for a `std::any`: `virtual_any>` bundles the `any` with the +v-table pointer for the value inside it, acquiring it once, on construction - +or not at all, when it is built from a value or by cpp:make_any_virtual[], +since the type is then known at compile time: + +```c++ +BOOST_OPENMETHOD(name, (const virtual_any&), std::string); + +// from an `any`: one lookup, at construction +virtual_any spot = erased(Dog{"Spot"}); + +// from a value, or with make_any_virtual: no lookup at all +virtual_any rex = Dog{"Rex"}; +auto snoopy = make_any_virtual(Dog{"Snoopy"}); +``` + +The Concept must contain `relaxed` - `virtual_any`'s default constructor and +assignment rely on it - and `copy_constructible<>`, for copies. + +For the same reason as for `std::any`, cpp:final_virtual_ptr[] is _deleted_ +for `type_erasure::any`: it would silently produce the v-table of the root +class rather than the one for the bound value. + +#### Empty `any`s + +An empty relaxed `any` reports `typeid(void)`, which is not a registered +class, so dispatching on it is a cpp:missing_class[] error. A catch-all +overrider does not help: dispatch never reaches it. Check with +`boost::type_erasure::is_empty` before calling. diff --git a/doc/modules/ROOT/snippets/type_erasure.cpp b/doc/modules/ROOT/snippets/type_erasure.cpp new file mode 100644 index 00000000..872247c7 --- /dev/null +++ b/doc/modules/ROOT/snippets/type_erasure.cpp @@ -0,0 +1,78 @@ +// Copyright (c) 2018-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#define BOOST_TEST_MODULE openmethod +#include + +#include "capture.hpp" + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +// tag::classes[] +// `relaxed` implies `typeid_<>`, which dispatch relies on. +using Concept = boost::mpl::vector, te::relaxed>; +using erased = te::any; + +struct Dog { + std::string name; +}; + +// The owning flavor, `any`, becomes the common base of the types +// the `any` may bind. +BOOST_OPENMETHOD_REGISTER( + use_type_erasure_types); +// end::classes[] + +// tag::method[] +BOOST_OPENMETHOD(name, (virtual_), std::string); + +// An overrider takes the bound value... +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { + return name; +} + +// ...or the `any` itself, which makes it a catch-all. +BOOST_OPENMETHOD_OVERRIDE(name, (const erased& value), std::string) { + return te::is_empty(value) ? "nothing" : "something else"; +} +// end::method[] + +BOOST_AUTO_TEST_CASE(type_erasure_examples) { + initialize(); + + { + capture_cout cout; + + // tag::dispatch[] + const erased spot(Dog{"Spot"}); + const erased answer(42); + + std::cout << name(spot) << "\n"; // Spot the dog + + // `int` is registered, but has no overrider of its own, so the + // catch-all applies. + std::cout << name(answer) << "\n"; // something else + // end::dispatch[] + + BOOST_TEST(cout.str() == "Spot the dog\nsomething else\n"); + } +} diff --git a/include/boost/openmethod/interop/boost_type_erasure.hpp b/include/boost/openmethod/interop/boost_type_erasure.hpp index ae0461a2..b5340069 100644 --- a/include/boost/openmethod/interop/boost_type_erasure.hpp +++ b/include/boost/openmethod/interop/boost_type_erasure.hpp @@ -125,6 +125,8 @@ struct validate_method_parameter< //! @tparam C The `any`'s Concept. //! @tparam T The `any`'s placeholder. //! @tparam Registry A @ref registry. +//! +//! @see [Interoperation with Boost.TypeErasure](xref:ROOT:interop_type_erasure.adoc) template struct virtual_traits&, Registry> { //! The type used for dispatch: the owning flavor for `C`. @@ -188,6 +190,8 @@ struct virtual_traits&, Registry> { //! @tparam C The `any`'s Concept. //! @tparam T The `any`'s placeholder. //! @tparam Registry A @ref registry. +//! +//! @see [Interoperation with Boost.TypeErasure](xref:ROOT:interop_type_erasure.adoc) template struct virtual_traits&, Registry> { //! The type used for dispatch: the owning flavor for `C`. @@ -251,6 +255,8 @@ struct virtual_traits&, Registry> { //! @tparam C The `any`'s Concept. //! @tparam T The `any`'s placeholder. //! @tparam Registry A @ref registry. +//! +//! @see [Interoperation with Boost.TypeErasure](xref:ROOT:interop_type_erasure.adoc) template struct virtual_traits&&, Registry> { //! The type used for dispatch: the owning flavor for `C`. @@ -324,6 +330,8 @@ struct virtual_traits&&, Registry> { //! @tparam C The `any`'s Concept. //! @tparam T The referent placeholder (`_self` for `any`). //! @tparam Registry A @ref registry. +//! +//! @see [Interoperation with Boost.TypeErasure](xref:ROOT:interop_type_erasure.adoc) template struct virtual_traits, Registry> { //! The type used for dispatch: the owning flavor for `C`. @@ -388,6 +396,8 @@ struct virtual_traits, Registry> { //! @tparam T The referent placeholder (`_self` for //! `any`). //! @tparam Registry A @ref registry. +//! +//! @see [Interoperation with Boost.TypeErasure](xref:ROOT:interop_type_erasure.adoc) template struct virtual_traits, Registry> { //! The type used for dispatch: the owning flavor for `C`. @@ -454,6 +464,11 @@ struct virtual_traits, Registry> { //! @tparam Any A `boost::type_erasure::any` type. //! @tparam T... The types that may be bound to the `any`, optionally //! followed by a @ref registry. +//! +//! @par Example +//! include:type_erasure.cpp#classes +//! +//! @see [Interoperation with Boost.TypeErasure](xref:ROOT:interop_type_erasure.adoc) template struct use_type_erasure_types : detail::use_any_types_aux< From 15ddee5f9e622fc964016d7626204c457ef84930 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 8 Aug 2026 16:13:58 -0400 Subject: [PATCH 23/64] doc: fix the broken type_erasure header link on the Headers page Co-Authored-By: Claude Fable 5 --- doc/modules/ROOT/pages/ref_headers.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/modules/ROOT/pages/ref_headers.adoc b/doc/modules/ROOT/pages/ref_headers.adoc index d694af7f..68fac536 100644 --- a/doc/modules/ROOT/pages/ref_headers.adoc +++ b/doc/modules/ROOT/pages/ref_headers.adoc @@ -110,7 +110,7 @@ parameters. Dispatch is on the type of the contained value. Also provides use the v-table of the `any` root class instead of the contained value's. [#boost_type_erasure] -### link:{{BASE_URL}}/include/boost/openmethod/interop/boost_type_erasure.hpp[] +### link:{base-url}/include/boost/openmethod/interop/boost_type_erasure.hpp[] Provides `virtual_traits` specializations that make it possible to use a `boost::type_erasure::any` in virtual parameters: the owning flavor by const, From 3bedb21abc22fa80324467316c25cda4d49a70f6 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 12:04:15 -0400 Subject: [PATCH 24/64] doc: document `vptr` in VirtualTraits and VptrFn `vptr` is a customization point at two levels, and neither was in the exposition-only blueprints: - `virtual_traits::vptr(arg)` - optional; lets a traits specialization override the default v-table lookup. - `policies::vptr::fn::vptr(type_id)` - the type-id-keyed lookup the above calls. Both arrived with 1eb22d8 ("inter-operate with 'any'") as `type_vptr`, renamed by f086985 and 7ecd96c; neither commit updated the blueprints. Document them, including when to implement them and why they exist, and mention in the `any` specializations that the vptr policy must provide `vptr(type_id)`. Also fix the detection of `virtual_traits::vptr`: it probed callability with a `type_id` (= `const void*`), which compiles for `std::any` and `boost::any` only because their converting constructors accept a `const void*`. An `any`-like type without such a constructor was silently ignored and fell back to `dynamic_vptr`, dispatching on the wrapper instead of the contained value. Probe with the actual argument type instead. Drive-bys: four `@ref policies::vptr::fn::dynamic_vptr` did not resolve (rendered as plain text) - use `@ref policies::VptrFn::dynamic_vptr`; drop a stray "a the" and align two stale std_any comments that claimed the rtti policy supplies the type id. Co-Authored-By: Claude Opus 5 (1M context) --- include/boost/openmethod/core.hpp | 41 ++++++++++++++++--- .../boost/openmethod/interop/boost_any.hpp | 12 ++++++ include/boost/openmethod/interop/std_any.hpp | 22 +++++++--- .../boost/openmethod/policies/vptr_map.hpp | 6 +-- .../boost/openmethod/policies/vptr_vector.hpp | 4 +- include/boost/openmethod/preamble.hpp | 21 +++++++++- 6 files changed, 89 insertions(+), 17 deletions(-) diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index bb496785..bd6fc90e 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -568,7 +568,7 @@ decltype(auto) acquire_vptr(const ArgType& arg) { return boost_openmethod_vptr(arg, static_cast(nullptr)); } else if constexpr (has_vptr< virtual_traits, - type_id>) { + const ArgType&>) { return virtual_traits::vptr(arg); } else { return Registry::template policy::dynamic_vptr(arg); @@ -788,7 +788,7 @@ class virtual_ptr { //! //! The pointer to the v-table is obtained by calling //! @ref boost_openmethod_vptr if a suitable overload exists, or the - //! @ref policies::vptr::fn::dynamic_vptr of the registry's + //! @ref policies::VptrFn::dynamic_vptr of the registry's //! `vptr` policy otherwise. //! //! @param other A reference to a polymorphic object @@ -823,7 +823,7 @@ class virtual_ptr { //! //! The pointer to the v-table is obtained by calling //! @ref boost_openmethod_vptr if a suitable overload exists, or the - //! @ref policies::vptr::fn::dynamic_vptr of the registry's + //! @ref policies::VptrFn::dynamic_vptr of the registry's //! `vptr` policy otherwise. //! //! @par Example @@ -892,7 +892,7 @@ class virtual_ptr { //! //! The pointer to the v-table is obtained by calling //! @ref boost_openmethod_vptr if a suitable overload exists, or the - //! @ref policies::vptr::fn::dynamic_vptr of the registry's + //! @ref policies::VptrFn::dynamic_vptr of the registry's //! `vptr` policy otherwise. //! //! @par Example @@ -930,7 +930,7 @@ class virtual_ptr { //! //! The pointer to the v-table is obtained by calling //! @ref boost_openmethod_vptr if a suitable overload exists, or the - //! @ref policies::vptr::fn::dynamic_vptr of the registry's + //! @ref policies::VptrFn::dynamic_vptr of the registry's //! `vptr` policy otherwise. //! //! @par Example @@ -2343,7 +2343,7 @@ BOOST_FORCEINLINE auto method::vptr( return boost_openmethod_vptr(obj, static_cast(nullptr)); } else if constexpr (detail::has_vptr< virtual_traits, - type_id>) { + decltype(obj)>) { return virtual_traits::vptr(obj); } else { return Registry::template policy::dynamic_vptr(obj); @@ -2721,6 +2721,35 @@ struct VirtualTraits { //! @return A reference to an object. static auto peek(T arg) -> const virtual_type&; + // Added by the `std::any` interop, under the name `type_vptr`. An `any` + // dispatches on the type of the value it contains, which the rtti policy + // cannot see: `dynamic_type` on the `any` itself yields the wrapper. + + //! Returns a *reference* to the v-table pointer for an object. + //! + //! `vptr` is optional. It is called on the object returned by @ref peek, + //! not on the method argument itself. A method acquires the v-table + //! pointer of a virtual argument from the first of the following that is + //! available: a `boost_openmethod_vptr` function, found by ADL on the + //! peeked object; `vptr`; @ref policies::VptrFn::dynamic_vptr of the + //! registry's @ref policies::vptr policy. + //! + //! Implement `vptr` only if the v-table pointer cannot be obtained from + //! the dynamic type of the peeked object, as reported by the registry's + //! @ref policies::rtti policy. This is the case for `any`-like types: + //! their dynamic type is the wrapper, not the value they contain. The + //! `std::any` specializations read the @ref type_id of the contained + //! value from `arg.type()`, and pass it to + //! @ref policies::VptrFn::vptr. + //! + //! `vptr` must return a *reference*, not a value, so that the caller + //! observes the current v-table pointer if the registry contains the + //! @ref policies::indirect_vptr policy and `initialize` is called again. + //! + //! @param arg The object returned by @ref peek. + //! @return A reference to the v-table pointer for `arg`. + static auto vptr(const virtual_type& arg) -> const vptr_type&; + //! Casts a virtual argument. //! //! `cast` is responsible for passing virtual arguments from method to diff --git a/include/boost/openmethod/interop/boost_any.hpp b/include/boost/openmethod/interop/boost_any.hpp index 6bab0463..2e932385 100644 --- a/include/boost/openmethod/interop/boost_any.hpp +++ b/include/boost/openmethod/interop/boost_any.hpp @@ -54,6 +54,10 @@ struct virtual_traits { //! `boost::any::type()` yields the same `std::type_info` object, provided //! Boost.TypeIndex uses `stl_type_index`. //! + //! Passes the type id to the registry's @ref policies::vptr policy, which + //! must provide @ref policies::VptrFn::vptr. Both + //! @ref policies::vptr_vector and @ref policies::vptr_map do. + //! //! If the registry has a @ref type_hash policy, uses it to convert the //! type id to an index; otherwise, uses the type_id as the index. //! @@ -125,6 +129,10 @@ struct virtual_traits { //! `boost::any::type()` yields the same `std::type_info` object, provided //! Boost.TypeIndex uses `stl_type_index`. //! + //! Passes the type id to the registry's @ref policies::vptr policy, which + //! must provide @ref policies::VptrFn::vptr. Both + //! @ref policies::vptr_vector and @ref policies::vptr_map do. + //! //! If the registry has a @ref type_hash policy, uses it to convert the //! type id to an index; otherwise, uses the type_id as the index. //! @@ -196,6 +204,10 @@ struct virtual_traits { //! `boost::any::type()` yields the same `std::type_info` object, provided //! Boost.TypeIndex uses `stl_type_index`. //! + //! Passes the type id to the registry's @ref policies::vptr policy, which + //! must provide @ref policies::VptrFn::vptr. Both + //! @ref policies::vptr_vector and @ref policies::vptr_map do. + //! //! If the registry has a @ref type_hash policy, uses it to convert the //! type id to an index; otherwise, uses the type_id as the index. //! diff --git a/include/boost/openmethod/interop/std_any.hpp b/include/boost/openmethod/interop/std_any.hpp index 73f05f71..125026c9 100644 --- a/include/boost/openmethod/interop/std_any.hpp +++ b/include/boost/openmethod/interop/std_any.hpp @@ -48,8 +48,12 @@ struct virtual_traits { //! Returns a *reference* to a v-table pointer for an object. //! - //! Acquires the dynamic @ref type_id of `arg`, using the registry's - //! @ref rtti policy. + //! Acquires the @ref type_id of the value stored in `arg`, using + //! `std::any::type()`. + //! + //! Passes it to the registry's @ref policies::vptr policy, which must + //! provide @ref policies::VptrFn::vptr. Both @ref policies::vptr_vector + //! and @ref policies::vptr_map do. //! //! If the registry has a @ref type_hash policy, uses it to convert the //! type id to an index; otherwise, uses the type_id as the index. @@ -111,6 +115,10 @@ struct virtual_traits { //! Acquires the @ref type_id of the value stored in `arg`, using //! `std::any::type()`. //! + //! Passes it to the registry's @ref policies::vptr policy, which must + //! provide @ref policies::VptrFn::vptr. Both @ref policies::vptr_vector + //! and @ref policies::vptr_map do. + //! //! If the registry has a @ref type_hash policy, uses it to convert the //! type id to an index; otherwise, uses the type_id as the index. //! @@ -169,8 +177,12 @@ struct virtual_traits { //! Returns a *reference* to a v-table pointer for an object. //! - //! Acquires the dynamic @ref type_id of `arg`, using the registry's - //! @ref rtti policy. + //! Acquires the @ref type_id of the value stored in `arg`, using + //! `std::any::type()`. + //! + //! Passes it to the registry's @ref policies::vptr policy, which must + //! provide @ref policies::VptrFn::vptr. Both @ref policies::vptr_vector + //! and @ref policies::vptr_map do. //! //! If the registry has a @ref type_hash policy, uses it to convert the //! type id to an index; otherwise, uses the type_id as the index. @@ -182,7 +194,7 @@ struct virtual_traits { //! terminates the program with @ref abort. //! //! @param arg A reference to a const `any`. - //! @return A reference to a the v-table pointer for `Class`. + //! @return A reference to the v-table pointer for the stored value. static auto vptr(const std::any& arg) -> const vptr_type& { return Registry::vptr::vptr(&arg.type()); } diff --git a/include/boost/openmethod/policies/vptr_map.hpp b/include/boost/openmethod/policies/vptr_map.hpp index 92588269..ef5d2c09 100644 --- a/include/boost/openmethod/policies/vptr_map.hpp +++ b/include/boost/openmethod/policies/vptr_map.hpp @@ -83,7 +83,7 @@ class vptr_map : public vptr { st().vptrs.swap(new_vptrs); } - //! Returns a reference to a v-table pointer for an object. + //! Returns a *reference* to a v-table pointer for an object. //! //! Acquires the dynamic @ref type_id of `arg`, using the registry's //! @ref rtti policy. @@ -96,7 +96,7 @@ class vptr_map : public vptr { //! //! @tparam Class A registered class. //! @param arg A reference to a const object of type `Class`. - //! @return A reference to a the v-table pointer for `Class`. + //! @return A reference to the v-table pointer for `Class`. template static auto dynamic_vptr(const Class& arg) -> const vptr_type& { return vptr(Registry::rtti::dynamic_type(arg)); @@ -111,7 +111,7 @@ class vptr_map : public vptr { //! terminates the program with @ref abort. //! //! @param type A `type_id`. - //! @return A reference to a the v-table pointer for `type`. + //! @return A reference to the v-table pointer for `type`. static auto vptr(type_id type) -> const vptr_type& { auto iter = st().vptrs.find(type); diff --git a/include/boost/openmethod/policies/vptr_vector.hpp b/include/boost/openmethod/policies/vptr_vector.hpp index 7496e414..304d2ab9 100644 --- a/include/boost/openmethod/policies/vptr_vector.hpp +++ b/include/boost/openmethod/policies/vptr_vector.hpp @@ -152,7 +152,7 @@ struct vptr_vector : vptr { //! //! @tparam Class A registered class. //! @param arg A reference to a const object of type `Class`. - //! @return A reference to a the v-table pointer for `Class`. + //! @return A reference to the v-table pointer for `Class`. template static auto dynamic_vptr(const Class& arg) -> const vptr_type& { return vptr(Registry::rtti::dynamic_type(arg)); @@ -170,7 +170,7 @@ struct vptr_vector : vptr { //! terminates the program with @ref abort. //! //! @param type A `type_id`. - //! @return A reference to a the v-table pointer for `type`. + //! @return A reference to the v-table pointer for `type`. static auto vptr(type_id type) -> const vptr_type& { std::size_t index; if constexpr (has_type_hash) { diff --git a/include/boost/openmethod/preamble.hpp b/include/boost/openmethod/preamble.hpp index 7fe9270d..0d53ee71 100644 --- a/include/boost/openmethod/preamble.hpp +++ b/include/boost/openmethod/preamble.hpp @@ -707,10 +707,29 @@ struct VptrFn { //! //! @tparam Class A registered class. //! @param arg A reference to a const object of type `Class`. - //! @return A reference to a the v-table pointer for `Class`. + //! @return A reference to the v-table pointer for `Class`. template static auto dynamic_vptr(const Class& arg) -> const vptr_type&; + // Added by the `std::any` interop, under the name `type_vptr`. An `any` + // knows the `type_id` of the value it contains, but has no object of that + // type to hand to `dynamic_vptr`. + + //! Return a *reference* to the v-table pointer for a type. + //! + //! Return a reference to the v-table pointer that `initialize` associated + //! to `type`. + //! + //! This function is optional. Implement it if the registry is to be used + //! with virtual parameters whose `virtual_traits` supply a `type_id` + //! themselves, instead of an object - see @ref VirtualTraits::vptr. Both + //! @ref vptr_vector and @ref vptr_map provide it, and implement + //! `dynamic_vptr` in terms of it. + //! + //! @param type A `type_id`. + //! @return A reference to the v-table pointer for `type`. + static auto vptr(type_id type) -> const vptr_type&; + //! Release the resources allocated by `initialize`. //! //! This function is optional. From a735c458854b52a85252f6b356c9987ceeb63c45 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 12:45:48 -0400 Subject: [PATCH 25/64] doc: make the `any` header links follow the deployment too The merge brought in the relative header links, but the three `any` interop headers were added on this branch and still pointed at the `base-url` attribute, which no longer exists. Convert them like the rest. Co-Authored-By: Claude Opus 5 (1M context) --- doc/modules/ROOT/pages/ref_headers.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/modules/ROOT/pages/ref_headers.adoc b/doc/modules/ROOT/pages/ref_headers.adoc index f62f8d80..2cbd7eed 100644 --- a/doc/modules/ROOT/pages/ref_headers.adoc +++ b/doc/modules/ROOT/pages/ref_headers.adoc @@ -77,7 +77,7 @@ Provides a `virtual_traits` specialization that makes it possible to use a `boost::intrusive_ptr` in place of a raw pointer or reference in virtual parameters. [#virtual_any] -### link:{base-url}/include/boost/openmethod/interop/virtual_any.hpp[] +### link:../../../include/boost/openmethod/interop/virtual_any.hpp[] Provides `virtual_any`, a wide `any` that combines an `any`, held by value, with a pointer to the v-table for the contained value - like `virtual_ptr` @@ -91,7 +91,7 @@ with an `any`-like interface, given `virtual_traits` specializations for its reference types. [#std_any] -### link:{base-url}/include/boost/openmethod/interop/std_any.hpp[] +### link:../../../include/boost/openmethod/interop/std_any.hpp[] Provides `virtual_traits` specializations that make it possible to use a `std::any` - by const reference, by mutable reference, or by rvalue reference - in virtual @@ -103,7 +103,7 @@ parameters. Dispatch is on the type of the contained value. Also provides use the v-table of the `any` root class instead of the contained value's. [#boost_any] -### link:{base-url}/include/boost/openmethod/interop/boost_any.hpp[] +### link:../../../include/boost/openmethod/interop/boost_any.hpp[] Provides `virtual_traits` specializations that make it possible to use a `boost::any` - by const reference, by mutable reference, or by rvalue reference - in virtual From 65904e39affbb403ba1be6d2a235d76c25de6903 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 13:02:24 -0400 Subject: [PATCH 26/64] doc: inline the final_virtual_ptr example instead of linking to it The explicit-`Registry` overload pointed at the default-registry overload's example with a hardcoded page name, `final_virtual_ptr-08.adoc`. MrDocs disambiguates overload pages with a content-derived hash, so adding the `any` overloads renamed that page to `final_virtual_ptr-08ea.adoc` and the link went dead. It went dead silently: mrdocs-addons rewrites `xref:reference:` into a plain `link:` on nested pages, to work around cppalliance/mrdocs#1245, and Antora does not validate a link macro. Any cross-reference to an *overload* page is therefore a link that rots without warning -- the two in macros.hpp are safe only because macro page names carry no hash. Pull in the snippet instead, with the same `include:` directive the default-registry overload already uses. There is no page name left to rot, and the example now comes from a file the build compiles and runs. Co-Authored-By: Claude Opus 5 (1M context) --- include/boost/openmethod/core.hpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index bd6fc90e..aada59b6 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -624,8 +624,7 @@ inline vptr_type null_vptr = nullptr; //! //! @par Example //! -//! See [the default-registry overload](xref:reference:boost/openmethod/final_virtual_ptr-08.adoc#_example) -//! for an example. +//! include:virtual_ptr.cpp#non_polymorphic_classes;final_virtual_ptr //! //! @tparam Registry A @ref registry. //! @tparam Arg The type of the argument. From 1fc8ea35bd676efad65223a89a56f37574500fd4 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 13:11:10 -0400 Subject: [PATCH 27/64] doc: hide the deleted `final_virtual_ptr` overloads from the reference The `any` headers delete twelve `final_virtual_ptr` overloads to stop the primary from silently using `static_vptr`. They are a guard, not API, and MrDocs gave each one its own page: the overload list went from 3 entries to 15. Guard them with `#ifndef __MRDOCS__`, as the friend declarations in core.hpp already are. The symbol is defined only while generating the reference, so the overloads are unchanged for every real compiler -- confirmed by compile_fail_final_virtual_ptr_std_any.cpp, which still fails with "use of deleted function". This also silences cppalliance/mrdocs#1251: the malformed link on the `aliases::final_virtual_ptr` page only appeared once the overload set grew, and the table is empty again now, so the Antora build is back to zero errors. Co-Authored-By: Claude Opus 5 (1M context) --- include/boost/openmethod/interop/boost_any.hpp | 5 +++++ include/boost/openmethod/interop/std_any.hpp | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/include/boost/openmethod/interop/boost_any.hpp b/include/boost/openmethod/interop/boost_any.hpp index 2e932385..9a707a62 100644 --- a/include/boost/openmethod/interop/boost_any.hpp +++ b/include/boost/openmethod/interop/boost_any.hpp @@ -322,7 +322,11 @@ make_boost_any_virtual(T&&... args) -> virtual_any { // from consideration when an explicit template argument list is given, so // the Registry-only templates - more specialized than the primary - catch // those. +// +// Hidden from the reference: they are a guard, not API, and six deleted +// overloads would crowd the `final_virtual_ptr` overload list. +#ifndef __MRDOCS__ template void final_virtual_ptr(const boost::any&) = delete; template @@ -332,6 +336,7 @@ void final_virtual_ptr(boost::any&&) = delete; void final_virtual_ptr(const boost::any&) = delete; void final_virtual_ptr(boost::any&) = delete; void final_virtual_ptr(boost::any&&) = delete; +#endif namespace aliases { using boost::openmethod::make_boost_any_virtual; diff --git a/include/boost/openmethod/interop/std_any.hpp b/include/boost/openmethod/interop/std_any.hpp index 125026c9..b87a9104 100644 --- a/include/boost/openmethod/interop/std_any.hpp +++ b/include/boost/openmethod/interop/std_any.hpp @@ -285,7 +285,11 @@ make_std_any_virtual(T&&... args) -> virtual_any { // from consideration when an explicit template argument list is given, so // the Registry-only templates - more specialized than the primary - catch // those. +// +// Hidden from the reference: they are a guard, not API, and six deleted +// overloads would crowd the `final_virtual_ptr` overload list. +#ifndef __MRDOCS__ template void final_virtual_ptr(const std::any&) = delete; template @@ -295,6 +299,7 @@ void final_virtual_ptr(std::any&&) = delete; void final_virtual_ptr(const std::any&) = delete; void final_virtual_ptr(std::any&) = delete; void final_virtual_ptr(std::any&&) = delete; +#endif namespace aliases { using boost::openmethod::make_std_any_virtual; From 597f03bfb8394974bf8fd6195c9c374426b82a6c Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 14:31:02 -0400 Subject: [PATCH 28/64] add the openmethod_vptr concept (#21) A Boost.TypeErasure concept that stores the v-table pointer for the bound type in the any's own dispatch table, and surfaces it as a boost_openmethod_vptr overload, which dispatch prefers over the vptr policy: calls through such an any - in every flavor - resolve in constant time, without hashing the result of typeid_of. Binding a value to the any also registers its type, as a class derived from the owning flavor, so use_type_erasure_types is unnecessary for these anys. Based on a design by Steven Watanabe (https://github.com/boostorg/openmethod/issues/21), modernized for the registry-era API: the two-argument intrinsic hook makes the registry a parameter of the concept instead of the identity of the class, and the virtual_traits shipped in this header replace the specialized rtti policy of the original. Co-Authored-By: Claude Fable 5 --- .../ROOT/examples/type_erasure_concept.cpp | 60 +++++++++++ .../ROOT/pages/interop_type_erasure.adoc | 39 +++++++- doc/modules/ROOT/pages/ref_headers.adoc | 11 ++- .../ROOT/snippets/type_erasure_concept.cpp | 62 ++++++++++++ .../openmethod/interop/boost_type_erasure.hpp | 99 +++++++++++++++++++ test/test_dispatch_type_erasure.cpp | 68 +++++++++++++ 6 files changed, 334 insertions(+), 5 deletions(-) create mode 100644 doc/modules/ROOT/examples/type_erasure_concept.cpp create mode 100644 doc/modules/ROOT/snippets/type_erasure_concept.cpp diff --git a/doc/modules/ROOT/examples/type_erasure_concept.cpp b/doc/modules/ROOT/examples/type_erasure_concept.cpp new file mode 100644 index 00000000..9bb261a8 --- /dev/null +++ b/doc/modules/ROOT/examples/type_erasure_concept.cpp @@ -0,0 +1,60 @@ +// Copyright (c) 2018-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) + +// clang-format off + +// tag::content[] +#include +#include + +#include +#include +#include +#include + +#include +#include + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +struct Dog { + std::string name; +}; + +// The concept must name the Concept it is part of, so the Concept is +// defined as a struct. +struct Dispatchable + : boost::mpl::vector< + te::copy_constructible<>, te::relaxed, + openmethod_vptr> {}; + +using erased = te::any; + +// No use_type_erasure_types: binding a value to the `any` registers its +// type. + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const erased& value), std::string) { + return te::is_empty(value) ? "nothing" : "something else"; +} + +#include + +int main() { + initialize(); + + const erased spot(Dog{"Spot"}); + const erased answer(42); + + std::cout << name(spot) << "\n"; // Spot the dog + std::cout << name(answer) << "\n"; // something else +} +// end::content[] diff --git a/doc/modules/ROOT/pages/interop_type_erasure.adoc b/doc/modules/ROOT/pages/interop_type_erasure.adoc index 957406ff..f7e7b6bf 100644 --- a/doc/modules/ROOT/pages/interop_type_erasure.adoc +++ b/doc/modules/ROOT/pages/interop_type_erasure.adoc @@ -123,9 +123,46 @@ For the same reason as for `std::any`, cpp:final_virtual_ptr[] is _deleted_ for `type_erasure::any`: it would silently produce the v-table of the root class rather than the one for the bound value. +#### The `openmethod_vptr` concept + +`virtual_any` removes the hash lookup by making the _object_ wider. +Boost.TypeErasure offers a way to remove it for plain, unwidened `any`\s: +since the `any` already carries a dispatch table of Concept operations, the +v-table pointer can be one of them. cpp:openmethod_vptr[] is a +Boost.TypeErasure concept that does exactly that. Include it in the Concept, +and every flavor of the `any` gains an operation that returns the registry's +static v-table pointer (`registry::static_vptr`) for the bound type - +instantiated once per bound type, retrieved in constant time. The concept surfaces the operation as a +`boost_openmethod_vptr` overload, which dispatch prefers over the `vptr` +policy, so calls through such an `any` never hash the result of `typeid_of`. + +Binding a value to such an `any` also _registers_ its type, as a class derived +from the owning flavor - the registrar is instantiated along with the +operation. `use_type_erasure_types` becomes unnecessary for these `any`\s, +although the two registration styles may coexist. + +The concept must name the Concept it is part of, so the Concept is defined as +a struct - the class name is already in scope in its own base-clause: + +[source,c++] +---- +include::example$type_erasure_concept.cpp[tag=content] +---- + +The price is coupling: the Concept must be OpenMethod-aware, and the registry +is part of the `any`'s type - whereas the `typeid_of`-based dispatch above +works with any pre-existing Concept containing `typeid_<>`. To use an `any` +with several registries, list the concept several times, once per registry: +`openmethod_vptr`. + +This concept is based on a design contributed by Steven Watanabe in +link:https://github.com/boostorg/openmethod/issues/21[issue #21]. + #### Empty `any`s An empty relaxed `any` reports `typeid(void)`, which is not a registered class, so dispatching on it is a cpp:missing_class[] error. A catch-all overrider does not help: dispatch never reaches it. Check with -`boost::type_erasure::is_empty` before calling. +`boost::type_erasure::is_empty` before calling. With the `openmethod_vptr` +concept, the failure mode differs: calling a concept operation on an empty +relaxed `any` throws `boost::type_erasure::bad_function_call`. diff --git a/doc/modules/ROOT/pages/ref_headers.adoc b/doc/modules/ROOT/pages/ref_headers.adoc index 68fac536..1de6f5b9 100644 --- a/doc/modules/ROOT/pages/ref_headers.adoc +++ b/doc/modules/ROOT/pages/ref_headers.adoc @@ -120,10 +120,13 @@ the type of the bound value, obtained via `boost::type_erasure::typeid_of`; the Concept must contain `boost::type_erasure::typeid_<>`, which `relaxed` implies. Also provides `use_type_erasure_types`, which registers the types that may be bound; `virtual_any>` works as well, and looks the v-table pointer -up only once, at construction. In addition, the header deletes the -`final_virtual_ptr` overloads for `boost::type_erasure::any`, which would -otherwise silently use the v-table of the `any` root class instead of the bound -value's. +up only once, at construction. `openmethod_vptr` - based on a design by Steven +Watanabe - is a Boost.TypeErasure concept that stores the v-table pointer for +the bound type in the `any`'s own dispatch table, removing the hash lookup +altogether, and registers bound types automatically. In addition, the header +deletes the `final_virtual_ptr` overloads for `boost::type_erasure::any`, which +would otherwise silently use the v-table of the `any` root class instead of the +bound value's. *The headers below are for advanced use*. diff --git a/doc/modules/ROOT/snippets/type_erasure_concept.cpp b/doc/modules/ROOT/snippets/type_erasure_concept.cpp new file mode 100644 index 00000000..969e695f --- /dev/null +++ b/doc/modules/ROOT/snippets/type_erasure_concept.cpp @@ -0,0 +1,62 @@ +// Copyright (c) 2018-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +#include + +#include +#include +#include + +#include +#include +#include + +#define BOOST_TEST_MODULE openmethod +#include + +#include "capture.hpp" + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +// tag::concept[] +struct Dog { + std::string name; +}; + +// The concept must name the Concept it is part of, so the Concept is +// defined as a struct. +struct Dispatchable : boost::mpl::vector< + te::copy_constructible<>, te::relaxed, + openmethod_vptr> {}; + +using erased = te::any; + +// No use_type_erasure_types: binding a value to the `any` registers its +// type. + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} +// end::concept[] + +BOOST_AUTO_TEST_CASE(type_erasure_concept_example) { + initialize(); + + { + capture_cout cout; + + // tag::dispatch[] + const erased spot(Dog{"Spot"}); + + std::cout << name(spot) << "\n"; // Spot the dog + // end::dispatch[] + + BOOST_TEST(cout.str() == "Spot the dog\n"); + } +} diff --git a/include/boost/openmethod/interop/boost_type_erasure.hpp b/include/boost/openmethod/interop/boost_type_erasure.hpp index b5340069..1ccfc610 100644 --- a/include/boost/openmethod/interop/boost_type_erasure.hpp +++ b/include/boost/openmethod/interop/boost_type_erasure.hpp @@ -8,7 +8,11 @@ #include #include +#include +#include #include +#include +#include #include #include @@ -40,6 +44,13 @@ // // The rvalue-reference flavor (`any`), and placeholders // other than `_self`, are not supported. +// +// In addition, `openmethod_vptr` - based on a design by Steven Watanabe - +// is a Boost.TypeErasure concept that stores the v-table pointer for the +// bound type in the any's own dispatch table, making every flavor of the +// any intrinsically polymorphic: calls resolve in constant time, without +// hashing the result of `typeid_of`, and binding a value to the any +// registers its type. namespace boost::openmethod { @@ -476,6 +487,93 @@ struct use_type_erasure_types detail::type_erasure_root, typename detail::extract_registry::others> {}; +namespace detail { + +// Registers Class as deriving from the owning flavor for Concept - the +// same shape use_type_erasure_types produces - when odr-used from +// openmethod_vptr::apply. +template +use_class_aux>> + use_type_erasure_class; + +} // namespace detail + +//! A Boost.TypeErasure concept that makes an `any` intrinsically +//! polymorphic. +//! +//! Including `openmethod_vptr` in a Concept adds an operation, +//! to the dispatch table of every flavor of `any`, that returns +//! the @ref registry::static_vptr for the bound type; and it surfaces the +//! operation as a @ref boost_openmethod_vptr overload, which dispatch +//! prefers over the registry's `vptr` policy. Calls thus resolve in +//! constant time, without hashing the result of +//! `boost::type_erasure::typeid_of`. +//! +//! In addition, binding a value to such an `any` registers its type as a +//! class derived from the owning flavor - the same shape +//! @ref use_type_erasure_types produces, with which it can coexist. No +//! explicit registration is needed for the types bound to `any`\s that +//! carry this concept. +//! +//! `Concept` must be the very Concept the `any` is instantiated with. +//! Since the concept appears inside that Concept, the Concept must name +//! itself: define it as a struct deriving from the concept list. +//! +//! Unlike the `vptr` policy, which reports a @ref missing_class error, +//! calling a method on an empty relaxed `any` throws +//! `boost::type_erasure::bad_function_call`. +//! +//! Based on a design by +//! [Steven Watanabe](https://github.com/boostorg/openmethod/issues/21). +//! +//! @tparam Concept The Concept containing this concept. +//! @tparam Registry A @ref registry. +//! @tparam T A placeholder; leave it to its default, `_self`. +//! +//! @par Example +//! include:type_erasure_concept.cpp#concept +//! +//! @see [Interoperation with Boost.TypeErasure](xref:ROOT:interop_type_erasure.adoc) +template< + class Concept, class Registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY, + typename T = boost::type_erasure::_self> +struct openmethod_vptr { + //! Returns the v-table pointer for the bound type. + //! + //! Also registers `T`, and the owning flavor for `Concept` as its + //! base, by odr-using their registrars. + //! + //! @return The @ref registry::static_vptr for `T`. + static auto apply(const T&) -> vptr_type { + (void)&detail::use_type_erasure_class< + Registry, boost::type_erasure::any, Concept>; + (void)&detail::use_type_erasure_class; + return Registry::template static_vptr; + } +}; + +} // namespace boost::openmethod + +namespace boost::type_erasure { + +// Surface the openmethod_vptr operation as the boost_openmethod_vptr +// intrinsic hook, injected into the interface of every flavor of any +// whose Concept contains the concept. +template +struct concept_interface< + boost::openmethod::openmethod_vptr, Base, T> : Base { + friend auto boost_openmethod_vptr( + const typename derived::type& arg, + Registry*) -> boost::openmethod::vptr_type { + return call( + boost::openmethod::openmethod_vptr(), arg); + } +}; + +} // namespace boost::type_erasure + +namespace boost::openmethod { + // The primary final_virtual_ptr would silently use the static v-table // pointer of the any class itself - the root -, not the bound value's. // Delete the combination. Both call forms need covering: the (C, T)-only @@ -497,6 +595,7 @@ template void final_virtual_ptr(boost::type_erasure::any&&) = delete; namespace aliases { +using boost::openmethod::openmethod_vptr; using boost::openmethod::use_type_erasure_types; } // namespace aliases diff --git a/test/test_dispatch_type_erasure.cpp b/test/test_dispatch_type_erasure.cpp index 9927671a..352d3ded 100644 --- a/test/test_dispatch_type_erasure.cpp +++ b/test/test_dispatch_type_erasure.cpp @@ -289,3 +289,71 @@ BOOST_AUTO_TEST_CASE(type_erasure_indirect_vptr) { BOOST_TEST(name_method::fn(rex.get()) == "Rex the dog"); } } // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// the openmethod_vptr concept (based on a design by Steven Watanabe): the +// any carries the v-table pointer for its bound type in its own dispatch +// table, and binding a value registers its type + +struct Dog { + std::string name; +}; + +// The concept must name the Concept it is part of: define the Concept as +// a struct. +struct Dispatchable : boost::mpl::vector< + te::copy_constructible<>, te::relaxed, + openmethod_vptr> {}; + +using dispatchable = te::any; +using dispatchable_ref = te::any; + +// the intrinsic hook is found for every flavor, so dispatch prefers it +// over the vptr policy's hash lookup +static_assert(detail::has_vptr_fn); +static_assert(detail::has_vptr_fn); + +// explicit registration is not needed, but may coexist (class dedup) +BOOST_OPENMETHOD_REGISTER(use_type_erasure_types); + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const dispatchable& value), std::string) { + return te::is_empty(value) ? "nothing" : "something"; +} + +BOOST_OPENMETHOD(poke, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(poke, (Dog & dog), std::string) { + dog.name += "!"; + return dog.name; +} + +BOOST_AUTO_TEST_CASE(type_erasure_openmethod_vptr_concept) { + initialize(trace()); + + const dispatchable spot(Dog{"Spot"}); + + // the hook returns the static vptr for the bound type + BOOST_TEST( + boost_openmethod_vptr(spot, static_cast(nullptr)) == + default_registry::static_vptr); + BOOST_TEST(name(spot) == "Spot the dog"); + + // std::string appears nowhere in this section; binding it registered + // it, and the catch-all applies + const dispatchable felix(std::string{"Felix"}); + BOOST_TEST(name(felix) == "something"); + + // the reference-wrapper flavor takes the fast path too + Dog snoopy{"Snoopy"}; + BOOST_TEST(poke(dispatchable_ref(snoopy)) == "Snoopy!"); + BOOST_TEST(snoopy.name == "Snoopy!"); +} +} // namespace BOOST_OPENMETHOD_GENSYM From 6c6cb440eb067c7abb68112dbf93f3d5c313dc93 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 14:31:23 -0400 Subject: [PATCH 29/64] doc: hide the deleted final_virtual_ptr overloads from the reference Same as for the any headers: they are a guard, not API, and six deleted overloads crowd the final_virtual_ptr overload list - and push it past the size that triggers the MrDocs "Introduced Symbols" URL bug on the aliases/final_virtual_ptr page. Co-Authored-By: Claude Fable 5 --- include/boost/openmethod/interop/boost_type_erasure.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/include/boost/openmethod/interop/boost_type_erasure.hpp b/include/boost/openmethod/interop/boost_type_erasure.hpp index 1ccfc610..6898b113 100644 --- a/include/boost/openmethod/interop/boost_type_erasure.hpp +++ b/include/boost/openmethod/interop/boost_type_erasure.hpp @@ -580,7 +580,11 @@ namespace boost::openmethod { // templates catch calls that deduce the default registry, and the // Registry-first templates catch explicit-registry calls; both are more // specialized than the primary's forwarding-reference parameter. +// +// Hidden from the reference: they are a guard, not API, and six deleted +// overloads would crowd the `final_virtual_ptr` overload list. +#ifndef __MRDOCS__ template void final_virtual_ptr(const boost::type_erasure::any&) = delete; template @@ -593,6 +597,7 @@ template void final_virtual_ptr(boost::type_erasure::any&) = delete; template void final_virtual_ptr(boost::type_erasure::any&&) = delete; +#endif namespace aliases { using boost::openmethod::openmethod_vptr; From 8737e867c0000447e8dd82ed28f195dbb018804d Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 14:45:20 -0400 Subject: [PATCH 30/64] doc: tighten the boost_type_erasure entry on the Headers page Co-Authored-By: Claude Fable 5 --- doc/modules/ROOT/pages/ref_headers.adoc | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/doc/modules/ROOT/pages/ref_headers.adoc b/doc/modules/ROOT/pages/ref_headers.adoc index 1de6f5b9..04b2bf99 100644 --- a/doc/modules/ROOT/pages/ref_headers.adoc +++ b/doc/modules/ROOT/pages/ref_headers.adoc @@ -112,21 +112,15 @@ use the v-table of the `any` root class instead of the contained value's. [#boost_type_erasure] ### link:{base-url}/include/boost/openmethod/interop/boost_type_erasure.hpp[] -Provides `virtual_traits` specializations that make it possible to use a -`boost::type_erasure::any` in virtual parameters: the owning flavor by const, -mutable or rvalue reference, and the reference-wrapper flavors -(`any`, `any`) by value. Dispatch is on -the type of the bound value, obtained via `boost::type_erasure::typeid_of`; the -Concept must contain `boost::type_erasure::typeid_<>`, which `relaxed` implies. -Also provides `use_type_erasure_types`, which registers the types that may be -bound; `virtual_any>` works as well, and looks the v-table pointer -up only once, at construction. `openmethod_vptr` - based on a design by Steven -Watanabe - is a Boost.TypeErasure concept that stores the v-table pointer for -the bound type in the `any`'s own dispatch table, removing the hash lookup -altogether, and registers bound types automatically. In addition, the header -deletes the `final_virtual_ptr` overloads for `boost::type_erasure::any`, which -would otherwise silently use the v-table of the `any` root class instead of the -bound value's. +Provides `virtual_traits` specializations for using a `boost::type_erasure::any` +in virtual parameters: the owning flavor by reference, the reference-wrapper +flavors by value. Dispatch is on the type of the bound value; the Concept must +contain `typeid_<>`, which `relaxed` implies. Also provides +`use_type_erasure_types`, which registers the types that may be bound, and +`openmethod_vptr`, a Boost.TypeErasure concept that stores the v-table pointer +in the `any`'s own dispatch table and registers bound types automatically. +`final_virtual_ptr` is deleted for `type_erasure::any`. Based on a design by +Steven Watanabe. *The headers below are for advanced use*. From 437c6662600b51a2d9e9952f119420aa3e02afac Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 14:50:44 -0400 Subject: [PATCH 31/64] reject virtual_ptr over classes with a boost_openmethod_vptr overload virtual_ptr and the intrinsic hook fill the same goal - fast access to the v-table pointer - so combining them buys nothing; and, with an indirect registry, it was outright broken: acquire_vptr preferred the hook, which returns the vptr by value, and box_vptr stored the address of the temporary - a dangling pointer read back on every dispatch (caught by ASan as stack-use-after-return). acquire_vptr is only called from virtual_ptr and virtual_any construction and assignment - dispatch uses method::vptr, which keeps the hook fast path. Make acquire_vptr static_assert that no hook applies, and drop its now-unreachable hook branch; the remaining branches (virtual_traits, vptr policy) return references into stable storage, so box_vptr is safe for everything acquire_vptr can return. Closes #87 Co-Authored-By: Claude Fable 5 --- doc/modules/ROOT/pages/virtual_ptr_alt.adoc | 4 ++ include/boost/openmethod/core.hpp | 57 ++++++++++++------- include/boost/openmethod/inplace_vptr.hpp | 4 ++ test/CMakeLists.txt | 3 + .../compile_fail_virtual_ptr_inplace_vptr.cpp | 22 +++++++ test/test_core.cpp | 14 ++--- 6 files changed, 74 insertions(+), 30 deletions(-) create mode 100644 test/compile_fail_virtual_ptr_inplace_vptr.cpp diff --git a/doc/modules/ROOT/pages/virtual_ptr_alt.adoc b/doc/modules/ROOT/pages/virtual_ptr_alt.adoc index 2c936031..eb5886f8 100644 --- a/doc/modules/ROOT/pages/virtual_ptr_alt.adoc +++ b/doc/modules/ROOT/pages/virtual_ptr_alt.adoc @@ -102,3 +102,7 @@ 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`. + +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/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index aada59b6..67fcf4da 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -562,13 +562,20 @@ BOOST_OPENMETHOD_DETAIL_HAS_STATIC_FN(vptr); template decltype(auto) acquire_vptr(const ArgType& arg) { + // A class with a boost_openmethod_vptr overload does not need to be + // wrapped: virtual_ptr and the hook fill the same goal, fast access + // to the v-table pointer. The hook also returns the vptr by value, + // which indirect registries cannot store (see box_vptr). + static_assert( + !has_vptr_fn, + "do not wrap an object that has a boost_openmethod_vptr overload " + "in a virtual_ptr; call methods directly on the object"); + Registry::require_initialized(); - if constexpr (has_vptr_fn) { - return boost_openmethod_vptr(arg, static_cast(nullptr)); - } else if constexpr (has_vptr< - virtual_traits, - const ArgType&>) { + if constexpr (has_vptr< + virtual_traits, + const ArgType&>) { return virtual_traits::vptr(arg); } else { return Registry::template policy::dynamic_vptr(arg); @@ -785,10 +792,12 @@ class virtual_ptr { //! Construct a `virtual_ptr` from a reference to an object //! - //! The pointer to the v-table is obtained by calling - //! @ref boost_openmethod_vptr if a suitable overload exists, or the - //! @ref policies::VptrFn::dynamic_vptr of the registry's - //! `vptr` policy otherwise. + //! The pointer to the v-table is obtained from @ref virtual_traits, + //! if it provides a `vptr` function, or from the + //! @ref policies::VptrFn::dynamic_vptr of the registry's `vptr` + //! policy otherwise. An object with a @ref boost_openmethod_vptr + //! overload is rejected at compile time: it carries its own v-table + //! pointer, and does not need to be wrapped in a `virtual_ptr`. //! //! @param other A reference to a polymorphic object //! @@ -820,10 +829,12 @@ class virtual_ptr { //! Construct a `virtual_ptr` from a pointer to an object //! - //! The pointer to the v-table is obtained by calling - //! @ref boost_openmethod_vptr if a suitable overload exists, or the - //! @ref policies::VptrFn::dynamic_vptr of the registry's - //! `vptr` policy otherwise. + //! The pointer to the v-table is obtained from @ref virtual_traits, + //! if it provides a `vptr` function, or from the + //! @ref policies::VptrFn::dynamic_vptr of the registry's `vptr` + //! policy otherwise. An object with a @ref boost_openmethod_vptr + //! overload is rejected at compile time: it carries its own v-table + //! pointer, and does not need to be wrapped in a `virtual_ptr`. //! //! @par Example //! include:virtual_ptr.cpp#ctor_pointer @@ -889,10 +900,12 @@ class virtual_ptr { //! Assign a `virtual_ptr` from a reference to an object //! - //! The pointer to the v-table is obtained by calling - //! @ref boost_openmethod_vptr if a suitable overload exists, or the - //! @ref policies::VptrFn::dynamic_vptr of the registry's - //! `vptr` policy otherwise. + //! The pointer to the v-table is obtained from @ref virtual_traits, + //! if it provides a `vptr` function, or from the + //! @ref policies::VptrFn::dynamic_vptr of the registry's `vptr` + //! policy otherwise. An object with a @ref boost_openmethod_vptr + //! overload is rejected at compile time: it carries its own v-table + //! pointer, and does not need to be wrapped in a `virtual_ptr`. //! //! @par Example //! include:virtual_ptr.cpp#assign_ref @@ -927,10 +940,12 @@ class virtual_ptr { //! Assign a `virtual_ptr` from a pointer to an object //! - //! The pointer to the v-table is obtained by calling - //! @ref boost_openmethod_vptr if a suitable overload exists, or the - //! @ref policies::VptrFn::dynamic_vptr of the registry's - //! `vptr` policy otherwise. + //! The pointer to the v-table is obtained from @ref virtual_traits, + //! if it provides a `vptr` function, or from the + //! @ref policies::VptrFn::dynamic_vptr of the registry's `vptr` + //! policy otherwise. An object with a @ref boost_openmethod_vptr + //! overload is rejected at compile time: it carries its own v-table + //! pointer, and does not need to be wrapped in a `virtual_ptr`. //! //! @par Example //! include:virtual_ptr.cpp#assign_pointer diff --git a/include/boost/openmethod/inplace_vptr.hpp b/include/boost/openmethod/inplace_vptr.hpp index 7505bf32..cf1328b0 100644 --- a/include/boost/openmethod/inplace_vptr.hpp +++ b/include/boost/openmethod/inplace_vptr.hpp @@ -76,6 +76,10 @@ class inplace_vptr_base_tag {}; //! @ref policies::vptr policy, nor any policy it depends on (like @ref //! policies::type_hash). //! +//! An object that embeds its v-table pointer does not need to be wrapped +//! in a @ref virtual_ptr - the two fill the same goal, fast access to the +//! v-table pointer - and wrapping one is rejected at compile time. +//! //! If `Registry` contains the @ref has_indirect_vptr policy, the v-table //! pointer is stored as a pointer to a pointer, and remains valid after a call //! to @ref initialize. diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6678cd47..a47bce34 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -169,6 +169,9 @@ openmethod_compile_fail_test( # "attempting to reference a deleted function" on MSVC. openmethod_compile_fail_test( compile_fail_final_virtual_ptr_std_any "deleted function") +openmethod_compile_fail_test( + compile_fail_virtual_ptr_inplace_vptr + "do not wrap an object that has a boost_openmethod_vptr overload") if (TARGET Boost::dll) add_subdirectory(dynamic_loading) diff --git a/test/compile_fail_virtual_ptr_inplace_vptr.cpp b/test/compile_fail_virtual_ptr_inplace_vptr.cpp new file mode 100644 index 00000000..f0043950 --- /dev/null +++ b/test/compile_fail_virtual_ptr_inplace_vptr.cpp @@ -0,0 +1,22 @@ +// Copyright (c) 2018-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 + +using namespace boost::openmethod; + +struct Animal : inplace_vptr_base { + virtual ~Animal() = default; +}; + +// An object with a boost_openmethod_vptr overload carries its own v-table +// pointer; wrapping it in a virtual_ptr is rejected at compile time. + +int main() { + Animal animal; + virtual_ptr p(animal); + return 0; +} diff --git a/test/test_core.cpp b/test/test_core.cpp index 5fd126b6..cc2d9695 100644 --- a/test/test_core.cpp +++ b/test/test_core.cpp @@ -283,20 +283,16 @@ namespace TEST_NS { using test_registry = test_registry_<__COUNTER__>; -const detail::word value; - struct Animal { - friend auto boost_openmethod_vptr(const Animal&, test_registry*) { - return &value; - } + friend auto + boost_openmethod_vptr(const Animal&, test_registry*) -> vptr_type; }; static_assert(detail::has_vptr_fn); static_assert(!detail::has_vptr_fn); -BOOST_AUTO_TEST_CASE(vptr_from_function) { - initialize(); - BOOST_TEST(detail::acquire_vptr(Animal{}) == &value); -} +// The hook serves dispatch (method::vptr), not virtual_ptr: acquire_vptr +// rejects classes with a boost_openmethod_vptr overload at compile time - +// see compile_fail_virtual_ptr_inplace_vptr.cpp. } // namespace TEST_NS From e3a2537b67ff797c970c8e4e3f8885f3167c6d4e Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 14:54:29 -0400 Subject: [PATCH 32/64] doc: openmethod_vptr anys are not wrapped in virtual_any Co-Authored-By: Claude Fable 5 --- doc/modules/ROOT/pages/interop_type_erasure.adoc | 4 ++++ doc/modules/ROOT/pages/ref_headers.adoc | 3 +-- include/boost/openmethod/interop/boost_type_erasure.hpp | 5 +++++ test/test_dispatch_type_erasure.cpp | 5 ++--- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/doc/modules/ROOT/pages/interop_type_erasure.adoc b/doc/modules/ROOT/pages/interop_type_erasure.adoc index f7e7b6bf..9f46e22e 100644 --- a/doc/modules/ROOT/pages/interop_type_erasure.adoc +++ b/doc/modules/ROOT/pages/interop_type_erasure.adoc @@ -155,6 +155,10 @@ works with any pre-existing Concept containing `typeid_<>`. To use an `any` with several registries, list the concept several times, once per registry: `openmethod_vptr`. +An `any` that carries the concept cannot be wrapped in a `virtual_any` - and +does not need to be: both fill the same goal, constant-time access to the +v-table pointer. Wrapping one is rejected at compile time. + This concept is based on a design contributed by Steven Watanabe in link:https://github.com/boostorg/openmethod/issues/21[issue #21]. diff --git a/doc/modules/ROOT/pages/ref_headers.adoc b/doc/modules/ROOT/pages/ref_headers.adoc index 4fbb8171..19137c79 100644 --- a/doc/modules/ROOT/pages/ref_headers.adoc +++ b/doc/modules/ROOT/pages/ref_headers.adoc @@ -124,8 +124,7 @@ contain `typeid_<>`, which `relaxed` implies. Also provides `use_type_erasure_types`, which registers the types that may be bound, and `openmethod_vptr`, a Boost.TypeErasure concept that stores the v-table pointer in the `any`'s own dispatch table and registers bound types automatically. -`final_virtual_ptr` is deleted for `type_erasure::any`. Based on a design by -Steven Watanabe. +`final_virtual_ptr` is deleted for `type_erasure::any`. *The headers below are for advanced use*. diff --git a/include/boost/openmethod/interop/boost_type_erasure.hpp b/include/boost/openmethod/interop/boost_type_erasure.hpp index 6898b113..feb97535 100644 --- a/include/boost/openmethod/interop/boost_type_erasure.hpp +++ b/include/boost/openmethod/interop/boost_type_erasure.hpp @@ -523,6 +523,11 @@ use_class_aux>> //! calling a method on an empty relaxed `any` throws //! `boost::type_erasure::bad_function_call`. //! +//! An `any` that carries this concept cannot be wrapped in a +//! @ref virtual_any - and does not need to be: both fill the same goal, +//! constant-time access to the v-table pointer. Wrapping one is rejected +//! at compile time. +//! //! Based on a design by //! [Steven Watanabe](https://github.com/boostorg/openmethod/issues/21). //! diff --git a/test/test_dispatch_type_erasure.cpp b/test/test_dispatch_type_erasure.cpp index 352d3ded..29929ee0 100644 --- a/test/test_dispatch_type_erasure.cpp +++ b/test/test_dispatch_type_erasure.cpp @@ -293,9 +293,8 @@ BOOST_AUTO_TEST_CASE(type_erasure_indirect_vptr) { namespace BOOST_OPENMETHOD_GENSYM { // ----------------------------------------------------------------------------- -// the openmethod_vptr concept (based on a design by Steven Watanabe): the -// any carries the v-table pointer for its bound type in its own dispatch -// table, and binding a value registers its type +// the openmethod_vptr concept: the any carries the v-table pointer for its +// bound type in its own dispatch table, and binding a value registers its type struct Dog { std::string name; From b1d78b8f9a5cf116deb6bb3bcd8248ef85cfcfe2 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 15:00:11 -0400 Subject: [PATCH 33/64] doc: make the type_erasure header link follow the deployment Co-Authored-By: Claude Fable 5 --- doc/modules/ROOT/pages/ref_headers.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/modules/ROOT/pages/ref_headers.adoc b/doc/modules/ROOT/pages/ref_headers.adoc index 19137c79..88e4950f 100644 --- a/doc/modules/ROOT/pages/ref_headers.adoc +++ b/doc/modules/ROOT/pages/ref_headers.adoc @@ -115,7 +115,7 @@ parameters. Dispatch is on the type of the contained value. Also provides use the v-table of the `any` root class instead of the contained value's. [#boost_type_erasure] -### link:{base-url}/include/boost/openmethod/interop/boost_type_erasure.hpp[] +### link:../../../include/boost/openmethod/interop/boost_type_erasure.hpp[] Provides `virtual_traits` specializations for using a `boost::type_erasure::any` in virtual parameters: the owning flavor by reference, the reference-wrapper From dbdd9511a2bb5d6fc144e62fbac2966e550993f6 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 15:08:40 -0400 Subject: [PATCH 34/64] doc: fix the plural any's rendering with literal backslashes and backticks Constrained monospace breaks when the closing backtick is followed by a letter, and the \s escape renders literally. Use unconstrained monospace for the plurals on the page, and reword the doc comment. Co-Authored-By: Claude Fable 5 --- doc/modules/ROOT/pages/interop_type_erasure.adoc | 10 +++++----- .../boost/openmethod/interop/boost_type_erasure.hpp | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/modules/ROOT/pages/interop_type_erasure.adoc b/doc/modules/ROOT/pages/interop_type_erasure.adoc index 9f46e22e..6b814041 100644 --- a/doc/modules/ROOT/pages/interop_type_erasure.adoc +++ b/doc/modules/ROOT/pages/interop_type_erasure.adoc @@ -20,8 +20,8 @@ the only requirement placed on the Concept is that it contain The types the `any` may bind have to be registered. cpp:use_type_erasure_types[] does that, registering the owning flavor - `any` - as a class, and each of the types as a class derived from it. -Each Concept gets a root of its own, so `any`s with different Concepts - and -the plain `any`s - can coexist in the same registry. A type that is not +Each Concept gets a root of its own, so ``any``s with different Concepts - and +the plain ``any``s - can coexist in the same registry. A type that is not registered cannot be dispatched on; a call with such a value bound to the `any` is a cpp:missing_class[] error - see xref:error_handling.adoc[Error Handling]. @@ -126,7 +126,7 @@ class rather than the one for the bound value. #### The `openmethod_vptr` concept `virtual_any` removes the hash lookup by making the _object_ wider. -Boost.TypeErasure offers a way to remove it for plain, unwidened `any`\s: +Boost.TypeErasure offers a way to remove it for plain, unwidened ``any``s: since the `any` already carries a dispatch table of Concept operations, the v-table pointer can be one of them. cpp:openmethod_vptr[] is a Boost.TypeErasure concept that does exactly that. Include it in the Concept, @@ -138,7 +138,7 @@ policy, so calls through such an `any` never hash the result of `typeid_of`. Binding a value to such an `any` also _registers_ its type, as a class derived from the owning flavor - the registrar is instantiated along with the -operation. `use_type_erasure_types` becomes unnecessary for these `any`\s, +operation. `use_type_erasure_types` becomes unnecessary for these ``any``s, although the two registration styles may coexist. The concept must name the Concept it is part of, so the Concept is defined as @@ -162,7 +162,7 @@ v-table pointer. Wrapping one is rejected at compile time. This concept is based on a design contributed by Steven Watanabe in link:https://github.com/boostorg/openmethod/issues/21[issue #21]. -#### Empty `any`s +#### Empty ``any``s An empty relaxed `any` reports `typeid(void)`, which is not a registered class, so dispatching on it is a cpp:missing_class[] error. A catch-all diff --git a/include/boost/openmethod/interop/boost_type_erasure.hpp b/include/boost/openmethod/interop/boost_type_erasure.hpp index feb97535..bfe8abbc 100644 --- a/include/boost/openmethod/interop/boost_type_erasure.hpp +++ b/include/boost/openmethod/interop/boost_type_erasure.hpp @@ -512,8 +512,8 @@ use_class_aux>> //! In addition, binding a value to such an `any` registers its type as a //! class derived from the owning flavor - the same shape //! @ref use_type_erasure_types produces, with which it can coexist. No -//! explicit registration is needed for the types bound to `any`\s that -//! carry this concept. +//! explicit registration is needed for the types bound to an `any` that +//! carries this concept. //! //! `Concept` must be the very Concept the `any` is instantiated with. //! Since the concept appears inside that Concept, the Concept must name From 516606ce7be796e51bfed740d661253c97e5f962 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 15:13:09 -0400 Subject: [PATCH 35/64] doc: trim the any-interop entries on the Headers page Co-Authored-By: Claude Sonnet 5 --- doc/modules/ROOT/pages/ref_headers.adoc | 32 +++++-------------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/doc/modules/ROOT/pages/ref_headers.adoc b/doc/modules/ROOT/pages/ref_headers.adoc index 2cbd7eed..efdb60ed 100644 --- a/doc/modules/ROOT/pages/ref_headers.adoc +++ b/doc/modules/ROOT/pages/ref_headers.adoc @@ -79,40 +79,20 @@ Provides a `virtual_traits` specialization that makes it possible to use a [#virtual_any] ### link:../../../include/boost/openmethod/interop/virtual_any.hpp[] -Provides `virtual_any`, a wide `any` that combines an `any`, held by value, -with a pointer to the v-table for the contained value - like `virtual_ptr` -combines a pointer to an object with a pointer to its v-table. The v-table -pointer is acquired when the `virtual_any` is created, so methods dispatch on -the contained type without looking it up on every call. Also provides -`make_any_virtual`, which creates a `virtual_any` containing a value of a -statically known type, setting the v-table pointer without any lookup. This -header is included by `std_any.hpp` and `boost_any.hpp`; it can serve any type -with an `any`-like interface, given `virtual_traits` specializations for its -reference types. +Provides `virtual_any`, a wide `any` that combines an `any`, held by value, with +a pointer to the v-table for the contained value - similar to `virtual_ptr`. [#std_any] ### link:../../../include/boost/openmethod/interop/std_any.hpp[] -Provides `virtual_traits` specializations that make it possible to use a `std::any` - -by const reference, by mutable reference, or by rvalue reference - in virtual -parameters. Dispatch is on the type of the contained value. Also provides -`use_std_any_types`, which registers the types that may be contained; -`virtual_std_any`, an alias for `virtual_any`, and -`make_std_any_virtual`. In addition, the header deletes the -`final_virtual_ptr` overloads for `std::any`, which would otherwise silently -use the v-table of the `any` root class instead of the contained value's. +Provides `virtual_traits` specializations that make it possible to use a +`std::any` in virtual parameters. [#boost_any] ### link:../../../include/boost/openmethod/interop/boost_any.hpp[] -Provides `virtual_traits` specializations that make it possible to use a `boost::any` - -by const reference, by mutable reference, or by rvalue reference - in virtual -parameters. Dispatch is on the type of the contained value. Also provides -`use_boost_any_types`, which registers the types that may be contained; -`virtual_boost_any`, an alias for `virtual_any`, and -`make_boost_any_virtual`. In addition, the header deletes the -`final_virtual_ptr` overloads for `boost::any`, which would otherwise silently -use the v-table of the `any` root class instead of the contained value's. +Provides `virtual_traits` specializations that make it possible to use a +`boost::any` in virtual parameters. *The headers below are for advanced use*. From c42b1bd47b5bfa481051f023538742817d77ba86 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 15:15:19 -0400 Subject: [PATCH 36/64] doc: trim the type_erasure entry on the Headers page to match Co-Authored-By: Claude Sonnet 5 --- doc/modules/ROOT/pages/ref_headers.adoc | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/doc/modules/ROOT/pages/ref_headers.adoc b/doc/modules/ROOT/pages/ref_headers.adoc index 2da0d85b..58cfd43b 100644 --- a/doc/modules/ROOT/pages/ref_headers.adoc +++ b/doc/modules/ROOT/pages/ref_headers.adoc @@ -97,14 +97,8 @@ Provides `virtual_traits` specializations that make it possible to use a [#boost_type_erasure] ### link:../../../include/boost/openmethod/interop/boost_type_erasure.hpp[] -Provides `virtual_traits` specializations for using a `boost::type_erasure::any` -in virtual parameters: the owning flavor by reference, the reference-wrapper -flavors by value. Dispatch is on the type of the bound value; the Concept must -contain `typeid_<>`, which `relaxed` implies. Also provides -`use_type_erasure_types`, which registers the types that may be bound, and -`openmethod_vptr`, a Boost.TypeErasure concept that stores the v-table pointer -in the `any`'s own dispatch table and registers bound types automatically. -`final_virtual_ptr` is deleted for `type_erasure::any`. +Provides specializations for using a `boost::type_erasure::any` in virtual +parameters. *The headers below are for advanced use*. From 9da7fd9b550565dddbf8b25a4fc7385897a4734b Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 15:31:46 -0400 Subject: [PATCH 37/64] doc: give the example Dog a ctor so make_any_virtual forwards its arguments make_any_virtual paren-constructs the Class from its arguments, and an aggregate cannot be paren-initialized in C++17. With a constructor, the tighter make_any_virtual("Snoopy") spelling works, and the test suite now exercises that forwarding path. Co-Authored-By: Claude Fable 5 --- doc/modules/ROOT/examples/type_erasure.cpp | 1 + doc/modules/ROOT/pages/interop_type_erasure.adoc | 2 +- test/test_dispatch_type_erasure.cpp | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/modules/ROOT/examples/type_erasure.cpp b/doc/modules/ROOT/examples/type_erasure.cpp index 21920eb4..e2819a9c 100644 --- a/doc/modules/ROOT/examples/type_erasure.cpp +++ b/doc/modules/ROOT/examples/type_erasure.cpp @@ -25,6 +25,7 @@ using Concept = boost::mpl::vector, te::relaxed>; using erased = te::any; struct Dog { + Dog(std::string name) : name(std::move(name)) {} std::string name; }; diff --git a/doc/modules/ROOT/pages/interop_type_erasure.adoc b/doc/modules/ROOT/pages/interop_type_erasure.adoc index 6b814041..20522dce 100644 --- a/doc/modules/ROOT/pages/interop_type_erasure.adoc +++ b/doc/modules/ROOT/pages/interop_type_erasure.adoc @@ -113,7 +113,7 @@ virtual_any spot = erased(Dog{"Spot"}); // from a value, or with make_any_virtual: no lookup at all virtual_any rex = Dog{"Rex"}; -auto snoopy = make_any_virtual(Dog{"Snoopy"}); +auto snoopy = make_any_virtual("Snoopy"); ``` The Concept must contain `relaxed` - `virtual_any`'s default constructor and diff --git a/test/test_dispatch_type_erasure.cpp b/test/test_dispatch_type_erasure.cpp index 29929ee0..f4563f41 100644 --- a/test/test_dispatch_type_erasure.cpp +++ b/test/test_dispatch_type_erasure.cpp @@ -32,6 +32,8 @@ static_assert(detail::has_vptr< #define MAKE_CLASSES() \ struct Dog { \ + Dog(std::string name) : name(std::move(name)) { \ + } \ std::string name; \ }; \ \ @@ -251,7 +253,7 @@ BOOST_AUTO_TEST_CASE(type_erasure_virtual_any) { BOOST_TEST(rex.vptr() == default_registry::static_vptr); BOOST_TEST(name(rex) == "Rex the dog"); - auto snoopy = make_any_virtual(Dog{"Snoopy"}); + auto snoopy = make_any_virtual("Snoopy"); BOOST_TEST(snoopy.vptr() == default_registry::static_vptr); BOOST_TEST(name(snoopy) == "Snoopy the dog"); } From b7706b366de6659c46110ace896c5e7e2a98b7e2 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 15:34:49 -0400 Subject: [PATCH 38/64] add a make_any_virtual overload that deduces the type of the value make_any_virtual(Dog{"Snoopy"}) now works: only the any type - and optionally the registry - needs to be spelled. The overload does not collide with the existing one: with a single explicit template argument, the existing overload's Any is neither deducible nor defaulted, and an is_registry constraint keeps the new one out of Class-and-Any calls. Co-Authored-By: Claude Fable 5 --- .../ROOT/pages/interop_type_erasure.adoc | 1 + doc/modules/ROOT/snippets/virtual_any.cpp | 12 +++++++++ .../boost/openmethod/interop/virtual_any.hpp | 27 +++++++++++++++++++ test/test_dispatch_type_erasure.cpp | 5 ++++ 4 files changed, 45 insertions(+) diff --git a/doc/modules/ROOT/pages/interop_type_erasure.adoc b/doc/modules/ROOT/pages/interop_type_erasure.adoc index 20522dce..0becd74d 100644 --- a/doc/modules/ROOT/pages/interop_type_erasure.adoc +++ b/doc/modules/ROOT/pages/interop_type_erasure.adoc @@ -114,6 +114,7 @@ virtual_any spot = erased(Dog{"Spot"}); // from a value, or with make_any_virtual: no lookup at all virtual_any rex = Dog{"Rex"}; auto snoopy = make_any_virtual("Snoopy"); +auto duke = make_any_virtual(Dog{"Duke"}); ``` The Concept must contain `relaxed` - `virtual_any`'s default constructor and diff --git a/doc/modules/ROOT/snippets/virtual_any.cpp b/doc/modules/ROOT/snippets/virtual_any.cpp index ad8e3713..c10a2fb6 100644 --- a/doc/modules/ROOT/snippets/virtual_any.cpp +++ b/doc/modules/ROOT/snippets/virtual_any.cpp @@ -156,6 +156,18 @@ BOOST_AUTO_TEST_CASE(std_any_examples) { BOOST_TEST(cout.str() == "Felix the cat\n"); } + { + capture_cout cout; + + // tag::make_any_virtual_value[] + auto felix = make_any_virtual(std::string("Felix the cat")); + + std::cout << name(felix) << "\n"; // Felix the cat + // end::make_any_virtual_value[] + + BOOST_TEST(cout.str() == "Felix the cat\n"); + } + { capture_cout cout; diff --git a/include/boost/openmethod/interop/virtual_any.hpp b/include/boost/openmethod/interop/virtual_any.hpp index 14f593e4..49511825 100644 --- a/include/boost/openmethod/interop/virtual_any.hpp +++ b/include/boost/openmethod/interop/virtual_any.hpp @@ -528,6 +528,33 @@ inline auto make_any_virtual(T&&... args) -> virtual_any { return virtual_any(Class(std::forward(args)...)); } +//! Create a `virtual_any` from an existing value. +//! +//! This overload deduces the type of the value: only the `any` type - and +//! optionally the registry - needs to be spelled. Stores `value` in a +//! @ref virtual_any, and sets the v-table pointer to the +//! @ref registry::static_vptr for `value`\'s type - no hash table lookup +//! is involved. If `value` is itself an `Any`, this is equivalent to the +//! `virtual_any` constructor taking an `any`, which looks the v-table +//! pointer up once. +//! +//! @tparam Any An `any` type. +//! @tparam Registry A @ref registry. +//! @tparam Class The type of the value (deduced). +//! @param value The value to store in the `any`. +//! @return A `virtual_any` containing `value`. +//! +//! @par Example +//! include:virtual_any.cpp#make_any_virtual_value +//! +//! @see [Interoperation with `any`](xref:ROOT:interop_any.adoc) +template< + class Any, class Registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY, class Class, + typename = std::enable_if_t>> +inline auto make_any_virtual(Class&& value) -> virtual_any { + return virtual_any(std::forward(value)); +} + namespace aliases { using boost::openmethod::make_any_virtual; using boost::openmethod::virtual_any; diff --git a/test/test_dispatch_type_erasure.cpp b/test/test_dispatch_type_erasure.cpp index f4563f41..14bee64d 100644 --- a/test/test_dispatch_type_erasure.cpp +++ b/test/test_dispatch_type_erasure.cpp @@ -256,6 +256,11 @@ BOOST_AUTO_TEST_CASE(type_erasure_virtual_any) { auto snoopy = make_any_virtual("Snoopy"); BOOST_TEST(snoopy.vptr() == default_registry::static_vptr); BOOST_TEST(name(snoopy) == "Snoopy the dog"); + + // from an existing value, deducing its type + auto duke = make_any_virtual(Dog{"Duke"}); + BOOST_TEST(duke.vptr() == default_registry::static_vptr); + BOOST_TEST(name(duke) == "Duke the dog"); } } // namespace BOOST_OPENMETHOD_GENSYM From 4b0c30aef77f750cd1264f42706e94f9dd0aeaac Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 15:56:43 -0400 Subject: [PATCH 39/64] Revert the value-deducing make_any_virtual overload and the example ctor Constructing the virtual_any directly from a value is just as direct and shorter; the factory forms added nothing. Show only the direct construction on the TypeErasure page. This reverts commits b7706b3 and 9da7fd9. Co-Authored-By: Claude Fable 5 --- doc/modules/ROOT/examples/type_erasure.cpp | 1 - .../ROOT/pages/interop_type_erasure.adoc | 8 +++--- doc/modules/ROOT/snippets/virtual_any.cpp | 12 --------- .../boost/openmethod/interop/virtual_any.hpp | 27 ------------------- test/test_dispatch_type_erasure.cpp | 9 +------ 5 files changed, 4 insertions(+), 53 deletions(-) diff --git a/doc/modules/ROOT/examples/type_erasure.cpp b/doc/modules/ROOT/examples/type_erasure.cpp index e2819a9c..21920eb4 100644 --- a/doc/modules/ROOT/examples/type_erasure.cpp +++ b/doc/modules/ROOT/examples/type_erasure.cpp @@ -25,7 +25,6 @@ using Concept = boost::mpl::vector, te::relaxed>; using erased = te::any; struct Dog { - Dog(std::string name) : name(std::move(name)) {} std::string name; }; diff --git a/doc/modules/ROOT/pages/interop_type_erasure.adoc b/doc/modules/ROOT/pages/interop_type_erasure.adoc index 0becd74d..4e374e47 100644 --- a/doc/modules/ROOT/pages/interop_type_erasure.adoc +++ b/doc/modules/ROOT/pages/interop_type_erasure.adoc @@ -102,8 +102,8 @@ Every call above looks the v-table up in a hash table, keyed on the type the `any` binds. cpp:virtual_any[] works for a `type_erasure::any` exactly as it does for a `std::any`: `virtual_any>` bundles the `any` with the v-table pointer for the value inside it, acquiring it once, on construction - -or not at all, when it is built from a value or by cpp:make_any_virtual[], -since the type is then known at compile time: +or not at all, when it is built from a value, since the type is then known at +compile time: ```c++ BOOST_OPENMETHOD(name, (const virtual_any&), std::string); @@ -111,10 +111,8 @@ BOOST_OPENMETHOD(name, (const virtual_any&), std::string); // from an `any`: one lookup, at construction virtual_any spot = erased(Dog{"Spot"}); -// from a value, or with make_any_virtual: no lookup at all +// from a value: no lookup at all virtual_any rex = Dog{"Rex"}; -auto snoopy = make_any_virtual("Snoopy"); -auto duke = make_any_virtual(Dog{"Duke"}); ``` The Concept must contain `relaxed` - `virtual_any`'s default constructor and diff --git a/doc/modules/ROOT/snippets/virtual_any.cpp b/doc/modules/ROOT/snippets/virtual_any.cpp index c10a2fb6..ad8e3713 100644 --- a/doc/modules/ROOT/snippets/virtual_any.cpp +++ b/doc/modules/ROOT/snippets/virtual_any.cpp @@ -156,18 +156,6 @@ BOOST_AUTO_TEST_CASE(std_any_examples) { BOOST_TEST(cout.str() == "Felix the cat\n"); } - { - capture_cout cout; - - // tag::make_any_virtual_value[] - auto felix = make_any_virtual(std::string("Felix the cat")); - - std::cout << name(felix) << "\n"; // Felix the cat - // end::make_any_virtual_value[] - - BOOST_TEST(cout.str() == "Felix the cat\n"); - } - { capture_cout cout; diff --git a/include/boost/openmethod/interop/virtual_any.hpp b/include/boost/openmethod/interop/virtual_any.hpp index 49511825..14f593e4 100644 --- a/include/boost/openmethod/interop/virtual_any.hpp +++ b/include/boost/openmethod/interop/virtual_any.hpp @@ -528,33 +528,6 @@ inline auto make_any_virtual(T&&... args) -> virtual_any { return virtual_any(Class(std::forward(args)...)); } -//! Create a `virtual_any` from an existing value. -//! -//! This overload deduces the type of the value: only the `any` type - and -//! optionally the registry - needs to be spelled. Stores `value` in a -//! @ref virtual_any, and sets the v-table pointer to the -//! @ref registry::static_vptr for `value`\'s type - no hash table lookup -//! is involved. If `value` is itself an `Any`, this is equivalent to the -//! `virtual_any` constructor taking an `any`, which looks the v-table -//! pointer up once. -//! -//! @tparam Any An `any` type. -//! @tparam Registry A @ref registry. -//! @tparam Class The type of the value (deduced). -//! @param value The value to store in the `any`. -//! @return A `virtual_any` containing `value`. -//! -//! @par Example -//! include:virtual_any.cpp#make_any_virtual_value -//! -//! @see [Interoperation with `any`](xref:ROOT:interop_any.adoc) -template< - class Any, class Registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY, class Class, - typename = std::enable_if_t>> -inline auto make_any_virtual(Class&& value) -> virtual_any { - return virtual_any(std::forward(value)); -} - namespace aliases { using boost::openmethod::make_any_virtual; using boost::openmethod::virtual_any; diff --git a/test/test_dispatch_type_erasure.cpp b/test/test_dispatch_type_erasure.cpp index 14bee64d..29929ee0 100644 --- a/test/test_dispatch_type_erasure.cpp +++ b/test/test_dispatch_type_erasure.cpp @@ -32,8 +32,6 @@ static_assert(detail::has_vptr< #define MAKE_CLASSES() \ struct Dog { \ - Dog(std::string name) : name(std::move(name)) { \ - } \ std::string name; \ }; \ \ @@ -253,14 +251,9 @@ BOOST_AUTO_TEST_CASE(type_erasure_virtual_any) { BOOST_TEST(rex.vptr() == default_registry::static_vptr); BOOST_TEST(name(rex) == "Rex the dog"); - auto snoopy = make_any_virtual("Snoopy"); + auto snoopy = make_any_virtual(Dog{"Snoopy"}); BOOST_TEST(snoopy.vptr() == default_registry::static_vptr); BOOST_TEST(name(snoopy) == "Snoopy the dog"); - - // from an existing value, deducing its type - auto duke = make_any_virtual(Dog{"Duke"}); - BOOST_TEST(duke.vptr() == default_registry::static_vptr); - BOOST_TEST(name(duke) == "Duke the dog"); } } // namespace BOOST_OPENMETHOD_GENSYM From 69b5d43bc6d76ba32f14ced602e15ede91a79ffd Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 17:05:37 -0400 Subject: [PATCH 40/64] remove the make_*_virtual factories make_any_virtual(args...) constructs the Class and moves it into the any - exactly what constructing the virtual_any from a value does, with more characters and one more name to learn; and constructing in place, the one thing a factory could add, is already covered by the emplace member. Remove make_any_virtual, make_std_any_virtual and make_boost_any_virtual. Co-Authored-By: Claude Fable 5 --- doc/modules/ROOT/pages/interop_any.adoc | 8 ++--- doc/modules/ROOT/snippets/virtual_any.cpp | 26 +--------------- .../boost/openmethod/interop/boost_any.hpp | 29 ------------------ include/boost/openmethod/interop/std_any.hpp | 29 ------------------ .../boost/openmethod/interop/virtual_any.hpp | 30 +------------------ test/compile_fail_virtual_any_by_value.cpp | 2 +- test/test_virtual_any_boost.cpp | 4 +-- test/test_virtual_any_std.cpp | 4 +-- 8 files changed, 10 insertions(+), 122 deletions(-) diff --git a/doc/modules/ROOT/pages/interop_any.adoc b/doc/modules/ROOT/pages/interop_any.adoc index d91e7c86..8af19b55 100644 --- a/doc/modules/ROOT/pages/interop_any.adoc +++ b/doc/modules/ROOT/pages/interop_any.adoc @@ -106,8 +106,7 @@ pointer, except that it _owns_ the object: the `any` is held by value. The pointer comes from a lookup when the `virtual_std_any` is built from an existing `any`, and from a static variable - no lookup at all - when it is built -from a value, or by cpp:make_std_any_virtual[], or by `emplace`, since the type -is then known at compile time. +from a value, or by `emplace`, since the type is then known at compile time. That makes it worthwhile when the same value is dispatched on repeatedly. Its usefulness is limited, though, by the fact that the wrapper is not what an @@ -127,9 +126,8 @@ the v-table of the `any` root class rather than the one for the contained value. #### `boost::any` `boost::any` is supported as well, by -``, with cpp:use_boost_any_types[], -cpp:virtual_boost_any[] and cpp:make_boost_any_virtual[] - the exact -counterparts of the constructs above. The two root classes are distinct, so +``, with cpp:use_boost_any_types[] and +cpp:virtual_boost_any[] - the exact counterparts of the constructs above. The two root classes are distinct, so `std::any` and `boost::any` may be used in the same program, and with the same registry. diff --git a/doc/modules/ROOT/snippets/virtual_any.cpp b/doc/modules/ROOT/snippets/virtual_any.cpp index ad8e3713..1d98b14a 100644 --- a/doc/modules/ROOT/snippets/virtual_any.cpp +++ b/doc/modules/ROOT/snippets/virtual_any.cpp @@ -143,30 +143,6 @@ BOOST_AUTO_TEST_CASE(std_any_examples) { BOOST_TEST(cout.str() == "Felix the cat\n"); } - - { - capture_cout cout; - - // tag::make_any_virtual[] - auto felix = make_any_virtual("Felix the cat"); - - std::cout << name(felix) << "\n"; // Felix the cat - // end::make_any_virtual[] - - BOOST_TEST(cout.str() == "Felix the cat\n"); - } - - { - capture_cout cout; - - // tag::make_std_any_virtual[] - auto felix = make_std_any_virtual("Felix the cat"); - - std::cout << name(felix) << "\n"; // Felix the cat - // end::make_std_any_virtual[] - - BOOST_TEST(cout.str() == "Felix the cat\n"); - } } BOOST_AUTO_TEST_CASE(boost_any_examples) { @@ -178,7 +154,7 @@ BOOST_AUTO_TEST_CASE(boost_any_examples) { capture_cout cout; // tag::boost_dispatch[] - auto felix = make_boost_any_virtual("Felix the cat"); + virtual_boost_any felix = std::string("Felix the cat"); std::cout << name(felix) << "\n"; // Felix the cat // end::boost_dispatch[] diff --git a/include/boost/openmethod/interop/boost_any.hpp b/include/boost/openmethod/interop/boost_any.hpp index 9a707a62..e8d58473 100644 --- a/include/boost/openmethod/interop/boost_any.hpp +++ b/include/boost/openmethod/interop/boost_any.hpp @@ -287,34 +287,6 @@ struct use_boost_any_types //! @see [Interoperation with `any`](xref:ROOT:interop_any.adoc) using virtual_boost_any = virtual_any; -//! Create a new object and return a `virtual_boost_any` containing it. -//! -//! Create a `Class` from `args`, store it in a `boost::any`, and return a -//! @ref virtual_any with its v-table pointer set to the -//! @ref registry::static_vptr for `Class` - no hash table lookup is -//! involved. -//! -//! @tparam Class The type of the value to create. -//! @tparam Registry A @ref registry. -//! @tparam T Types of the arguments to pass to the constructor of -//! `Class`. -//! @param args Arguments to pass to the constructor of `Class`. -//! @return A `virtual_any` containing a newly created -//! `Class`. -//! -//! @par Example -//! include:virtual_any.cpp#boost_dispatch -//! -//! @see [Interoperation with `any`](xref:ROOT:interop_any.adoc) -template< - class Class, class Registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY, - typename... T> -inline auto -make_boost_any_virtual(T&&... args) -> virtual_any { - return make_any_virtual( - std::forward(args)...); -} - // The primary final_virtual_ptr would silently use static_vptr // - the v-table of the `any` root class, not of the contained value. // Delete the combination. Both call forms need covering: the non-template @@ -339,7 +311,6 @@ void final_virtual_ptr(boost::any&&) = delete; #endif namespace aliases { -using boost::openmethod::make_boost_any_virtual; using boost::openmethod::use_boost_any_types; using boost::openmethod::virtual_boost_any; } // namespace aliases diff --git a/include/boost/openmethod/interop/std_any.hpp b/include/boost/openmethod/interop/std_any.hpp index b87a9104..54772417 100644 --- a/include/boost/openmethod/interop/std_any.hpp +++ b/include/boost/openmethod/interop/std_any.hpp @@ -250,34 +250,6 @@ struct use_std_any_types //! @see [Interoperation with `any`](xref:ROOT:interop_any.adoc) using virtual_std_any = virtual_any; -//! Create a new object and return a `virtual_std_any` containing it. -//! -//! Create a `Class` from `args`, store it in a `std::any`, and return a -//! @ref virtual_any with its v-table pointer set to the -//! @ref registry::static_vptr for `Class` - no hash table lookup is -//! involved. -//! -//! @tparam Class The type of the value to create. -//! @tparam Registry A @ref registry. -//! @tparam T Types of the arguments to pass to the constructor of -//! `Class`. -//! @param args Arguments to pass to the constructor of `Class`. -//! @return A `virtual_any` containing a newly created -//! `Class`. -//! -//! @par Example -//! include:virtual_any.cpp#make_std_any_virtual -//! -//! @see [Interoperation with `any`](xref:ROOT:interop_any.adoc) -template< - class Class, class Registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY, - typename... T> -inline auto -make_std_any_virtual(T&&... args) -> virtual_any { - return make_any_virtual( - std::forward(args)...); -} - // The primary final_virtual_ptr would silently use static_vptr // - the v-table of the `any` root class, not of the contained value. // Delete the combination. Both call forms need covering: the non-template @@ -302,7 +274,6 @@ void final_virtual_ptr(std::any&&) = delete; #endif namespace aliases { -using boost::openmethod::make_std_any_virtual; using boost::openmethod::use_std_any_types; using boost::openmethod::virtual_std_any; } // namespace aliases diff --git a/include/boost/openmethod/interop/virtual_any.hpp b/include/boost/openmethod/interop/virtual_any.hpp index 6a9e1171..3bfe99f0 100644 --- a/include/boost/openmethod/interop/virtual_any.hpp +++ b/include/boost/openmethod/interop/virtual_any.hpp @@ -38,8 +38,7 @@ struct is_virtual_any_aux> : std::true_type {}; //! either from the dynamic type of an existing `any` (a hash table //! lookup, via `virtual_traits::vptr`), or //! statically, when the contained type is known at compile time (the -//! value constructor, @ref make_any_virtual, and @ref emplace use @ref -//! registry::static_vptr). +//! value constructor and @ref emplace use @ref registry::static_vptr). //! //! Methods take `virtual_any` parameters by reference: `const //! virtual_any&`, `virtual_any&` or `virtual_any&&`. Overriders receive @@ -491,34 +490,7 @@ struct select_overrider_virtual_type_aux< } // namespace detail -//! Create a new object and return a `virtual_any` containing it. -//! -//! Create a `Class` from `args`, store it in a @ref virtual_any, and set -//! the v-table pointer to the @ref registry::static_vptr for `Class` - no -//! hash table lookup is involved. -//! -//! @tparam Class The type of the value to create. -//! @tparam Any An `any` type. -//! @tparam Registry A @ref registry. -//! @tparam T Types of the arguments to pass to the constructor of -//! `Class`. -//! @param args Arguments to pass to the constructor of `Class`. -//! @return A `virtual_any` containing a newly created -//! `Class`. -//! -//! @par Example -//! include:virtual_any.cpp#make_any_virtual -//! -//! @see [Interoperation with `any`](xref:ROOT:interop_any.adoc) -template< - class Class, class Any, class Registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY, - typename... T> -inline auto make_any_virtual(T&&... args) -> virtual_any { - return virtual_any(Class(std::forward(args)...)); -} - namespace aliases { -using boost::openmethod::make_any_virtual; using boost::openmethod::virtual_any; } // namespace aliases diff --git a/test/compile_fail_virtual_any_by_value.cpp b/test/compile_fail_virtual_any_by_value.cpp index 22afdf0b..fa30d78e 100644 --- a/test/compile_fail_virtual_any_by_value.cpp +++ b/test/compile_fail_virtual_any_by_value.cpp @@ -22,6 +22,6 @@ BOOST_OPENMETHOD_REGISTER(use_std_any_types); BOOST_OPENMETHOD(name, (virtual_std_any), std::string); int main() { - auto dog = make_std_any_virtual(Dog{"Snoopy"}); + virtual_std_any dog = Dog{"Snoopy"}; return name(dog).size(); } diff --git a/test/test_virtual_any_boost.cpp b/test/test_virtual_any_boost.cpp index a77b2e37..a9e87588 100644 --- a/test/test_virtual_any_boost.cpp +++ b/test/test_virtual_any_boost.cpp @@ -64,7 +64,7 @@ BOOST_AUTO_TEST_CASE(virtual_any_by_const_ref) { BOOST_TEST(rex.vptr() == default_registry::static_vptr); BOOST_TEST(name(rex) == "Rex the dog"); - auto felix = make_boost_any_virtual("Felix the cat"); + virtual_boost_any felix = std::string("Felix the cat"); BOOST_TEST(felix.vptr() == default_registry::static_vptr); BOOST_TEST(name(felix) == "Felix the cat"); @@ -151,7 +151,7 @@ BOOST_AUTO_TEST_CASE(virtual_any_by_xvalue_ref) { BOOST_TEST(boost::any_cast(spot.get()).name == ""); BOOST_TEST( - steal(make_boost_any_virtual("Felix the cat")) == + steal(virtual_boost_any(std::string("Felix the cat"))) == "Felix the cat"); } } // namespace BOOST_OPENMETHOD_GENSYM diff --git a/test/test_virtual_any_std.cpp b/test/test_virtual_any_std.cpp index 9cefe118..bb41a1b7 100644 --- a/test/test_virtual_any_std.cpp +++ b/test/test_virtual_any_std.cpp @@ -64,7 +64,7 @@ BOOST_AUTO_TEST_CASE(virtual_any_by_const_ref) { BOOST_TEST(rex.vptr() == default_registry::static_vptr); BOOST_TEST(name(rex) == "Rex the dog"); - auto felix = make_std_any_virtual("Felix the cat"); + virtual_std_any felix = std::string("Felix the cat"); BOOST_TEST(felix.vptr() == default_registry::static_vptr); BOOST_TEST(name(felix) == "Felix the cat"); @@ -151,7 +151,7 @@ BOOST_AUTO_TEST_CASE(virtual_any_by_xvalue_ref) { BOOST_TEST(std::any_cast(spot.get()).name == ""); BOOST_TEST( - steal(make_std_any_virtual("Felix the cat")) == + steal(virtual_std_any(std::string("Felix the cat"))) == "Felix the cat"); } } // namespace BOOST_OPENMETHOD_GENSYM From df3c0b7e0f14f866cc0c7791fa0c1ab76a99aee0 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 17:10:58 -0400 Subject: [PATCH 41/64] add virtual_any_ref, a non-owning counterpart of virtual_any virtual_any_ref borrows an existing any instead of holding a copy, and carries the v-table pointer for the contained value: a cheap, two-word handle with pointer semantics, passed to methods by value - like the reference-wrapper flavors of Boost.TypeErasure's any. The v-table pointer is acquired once, when the handle is created, or taken at no cost from a virtual_any. Any may be const-qualified; a mutable handle converts to a const one. Since a plain value does not convert to a virtual_any_ref, overriders that take the contained value are registered with the core API; the catch-all, which takes the handle itself, can use the macro. Co-Authored-By: Claude Fable 5 --- doc/modules/ROOT/pages/interop_any.adoc | 35 +++ doc/modules/ROOT/pages/ref_headers.adoc | 2 + doc/modules/ROOT/snippets/virtual_any.cpp | 44 ++++ .../boost/openmethod/interop/virtual_any.hpp | 242 ++++++++++++++++++ test/CMakeLists.txt | 3 + test/compile_fail_virtual_any_ref_by_ref.cpp | 27 ++ test/test_virtual_any_ref.cpp | 177 +++++++++++++ 7 files changed, 530 insertions(+) create mode 100644 test/compile_fail_virtual_any_ref_by_ref.cpp create mode 100644 test/test_virtual_any_ref.cpp diff --git a/doc/modules/ROOT/pages/interop_any.adoc b/doc/modules/ROOT/pages/interop_any.adoc index 8af19b55..d11797f8 100644 --- a/doc/modules/ROOT/pages/interop_any.adoc +++ b/doc/modules/ROOT/pages/interop_any.adoc @@ -123,6 +123,41 @@ For the same reason that a `virtual_std_any` caches what a plain `any` does not, cpp:final_virtual_ptr[] is _deleted_ for `std::any`: it would silently produce the v-table of the `any` root class rather than the one for the contained value. +#### `virtual_any_ref` + +`virtual_std_any` owns its `any`. cpp:virtual_any_ref[] is its non-owning +counterpart: it _borrows_ an `any` that lives elsewhere, bundling its address +with the v-table pointer for the value inside it - acquired once, when the +handle is created, or taken at no cost from a `virtual_any`. It is a cheap, +two-word handle with pointer semantics. Unlike the owning wrapper, it is passed +to methods _by value_, like the reference-wrapper flavors of Boost.TypeErasure's +`any`: + +```c++ +BOOST_OPENMETHOD(poke, (virtual_any_ref), std::string); + +std::any spot_any = Dog{"Spot"}; +virtual_any_ref spot = spot_any; // one lookup + +poke(spot); // no lookup +poke(spot); // no lookup; mutations reach spot_any +``` + +`Any` may be const-qualified: through `virtual_any_ref`, +overriders receive the contained value by value or by const reference only. A +mutable handle converts to a const one. + +A plain value does not convert to a `virtual_any_ref` - there is no `any` for +the handle to borrow - so `BOOST_OPENMETHOD_OVERRIDE`, which locates the method +by convertibility, cannot register overriders that take the contained value. +Register them with the core API instead, as in the `virtual_` case +above; the catch-all overrider, which takes the handle itself, can use the +macro. + +The handle does not track its referent: if the value inside the `any` is +replaced, the handle is stale - like an iterator into a modified container - +and must be re-created. + #### `boost::any` `boost::any` is supported as well, by diff --git a/doc/modules/ROOT/pages/ref_headers.adoc b/doc/modules/ROOT/pages/ref_headers.adoc index efdb60ed..7f195a1a 100644 --- a/doc/modules/ROOT/pages/ref_headers.adoc +++ b/doc/modules/ROOT/pages/ref_headers.adoc @@ -81,6 +81,8 @@ Provides a `virtual_traits` specialization that makes it possible to use a Provides `virtual_any`, a wide `any` that combines an `any`, held by value, with a pointer to the v-table for the contained value - similar to `virtual_ptr`. +Also provides `virtual_any_ref`, a non-owning counterpart that borrows an +existing `any`. [#std_any] ### link:../../../include/boost/openmethod/interop/std_any.hpp[] diff --git a/doc/modules/ROOT/snippets/virtual_any.cpp b/doc/modules/ROOT/snippets/virtual_any.cpp index 1d98b14a..b832239b 100644 --- a/doc/modules/ROOT/snippets/virtual_any.cpp +++ b/doc/modules/ROOT/snippets/virtual_any.cpp @@ -78,6 +78,28 @@ BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { } // namespace boost_any +namespace any_ref { + +using std_any::Dog; + +// tag::ref[] +BOOST_OPENMETHOD(poke, (virtual_any_ref), std::string); + +// A plain value does not convert to a virtual_any_ref, so overriders +// that take the contained value are registered with the core API. +using poke_method = + BOOST_OPENMETHOD_TYPE(poke, (virtual_any_ref), std::string); + +auto poke_dog(Dog& dog) -> std::string { + dog.name += "!"; + return dog.name; +} + +BOOST_OPENMETHOD_REGISTER(poke_method::override); +// end::ref[] + +} // namespace any_ref + BOOST_AUTO_TEST_CASE(std_any_examples) { using namespace std_any; @@ -145,6 +167,28 @@ BOOST_AUTO_TEST_CASE(std_any_examples) { } } +BOOST_AUTO_TEST_CASE(virtual_any_ref_examples) { + using namespace any_ref; + + initialize(); + + { + capture_cout cout; + + // tag::ref_dispatch[] + std::any spot_any = Dog{"Spot"}; + + // one lookup; the handle borrows the `any` + virtual_any_ref spot = spot_any; + + std::cout << poke(spot) << "\n"; // Spot! + std::cout << poke(spot) << "\n"; // Spot!! - no lookup on any call + // end::ref_dispatch[] + + BOOST_TEST(cout.str() == "Spot!\nSpot!!\n"); + } +} + BOOST_AUTO_TEST_CASE(boost_any_examples) { using namespace boost_any; diff --git a/include/boost/openmethod/interop/virtual_any.hpp b/include/boost/openmethod/interop/virtual_any.hpp index 3bfe99f0..109e1b00 100644 --- a/include/boost/openmethod/interop/virtual_any.hpp +++ b/include/boost/openmethod/interop/virtual_any.hpp @@ -16,6 +16,9 @@ namespace boost::openmethod { template class virtual_any; +template +class virtual_any_ref; + namespace detail { template @@ -24,6 +27,9 @@ struct is_virtual_any_aux : std::false_type {}; template struct is_virtual_any_aux> : std::true_type {}; +template +struct is_virtual_any_aux> : std::true_type {}; + } // namespace detail //! A wide `any`, combining an `any` and a pointer to a v-table. @@ -71,6 +77,9 @@ class virtual_any { template friend struct virtual_traits; + template + friend class virtual_any_ref; + public: //! Construct an empty `virtual_any`. //! @@ -490,8 +499,241 @@ struct select_overrider_virtual_type_aux< } // namespace detail +//! A wide reference to an `any`: a pointer to an `any`, and a pointer to +//! a v-table. +//! +//! `virtual_any_ref` is the non-owning counterpart of @ref virtual_any: +//! it *borrows* an existing `any` instead of holding a copy, and carries +//! the v-table pointer for the contained value, so methods dispatch on +//! the contained type without looking it up on every call. It is a +//! cheap, two-word handle with pointer semantics - copying it copies the +//! two words - and, like the reference-wrapper flavors of +//! Boost.TypeErasure's `any`, it is passed to methods *by value*. +//! +//! `Any` may be const-qualified: through `virtual_any_ref`, +//! overriders can only take the contained value by value or by const +//! reference; `virtual_any_ref` also supports mutable references, +//! and modifications reach the referent. A `virtual_any_ref` +//! converts to a `virtual_any_ref`. +//! +//! The v-table pointer is acquired when the handle is created: from the +//! dynamic type of the value contained in the `any` (a hash table +//! lookup), or at no cost from a @ref virtual_any, which already carries +//! it. The handle does not track its referent: if the value inside the +//! `any` is replaced, the handle is stale - like an iterator into a +//! modified container - and must be re-created. +//! +//! An overrider takes the *contained* value - or, for a catch-all +//! overrider, the `virtual_any_ref` itself, by value. Since a plain +//! value does not convert to a `virtual_any_ref`, overriders taking the +//! contained value are registered with the core API +//! (`method<...>::override`) rather than with +//! @ref BOOST_OPENMETHOD_OVERRIDE, which locates the method by +//! convertibility. +//! +//! `Any` can be `std::any`, `boost::any`, or any type that has an +//! `any`-like interface, and specializes `virtual_traits` for its +//! reference types, providing `vptr` and `cast`. +//! +//! @tparam Any An `any` type, possibly const-qualified. +//! @tparam Registry A @ref registry. +//! +//! @par Example +//! include:virtual_any.cpp#ref;ref_dispatch +//! +//! @see [Interoperation with `any`](xref:ROOT:interop_any.adoc) +template +class virtual_any_ref { + static constexpr bool use_indirect_vptrs = Registry::has_indirect_vptr; + + using owner_type = std::conditional_t< + std::is_const_v, + const virtual_any, Registry>, + virtual_any, Registry>>; + + Any* obj; + std::conditional_t vp; + + template + friend struct virtual_traits; + + template + friend class virtual_any_ref; + + public: + //! Construct from an `any`. + //! + //! Acquires the v-table pointer for the contained value, using + //! `virtual_traits::vptr` - a hash table + //! lookup. + //! + //! @param other An `any` lvalue. + virtual_any_ref(Any& other) + : obj(&other), vp(detail::box_vptr( + detail::acquire_vptr(other))) { + } + + //! A `virtual_any_ref` cannot borrow a temporary `any`. + virtual_any_ref(std::remove_const_t&&) = delete; + + //! Construct from a `virtual_any`. + //! + //! Borrows the `any` held by `other`, and copies its v-table pointer + //! - no lookup is involved. A `virtual_any_ref` can + //! borrow from a const `virtual_any`; a mutable one requires a + //! mutable `virtual_any`. + //! + //! @param other A `virtual_any` lvalue. + virtual_any_ref(owner_type& other) : obj(&other.obj), vp(other.vp) { + } + + //! A `virtual_any_ref` cannot borrow a temporary `virtual_any`. + virtual_any_ref(std::remove_const_t&&) = delete; + + //! Convert a mutable `virtual_any_ref` to a const one. + template< + class Other, + typename = std::enable_if_t< + std::is_const_v && + std::is_same_v>>> + virtual_any_ref(virtual_any_ref other) + : obj(other.obj), vp(other.vp) { + } + + //! Return a reference to the (non-modifiable) `any`. + auto get() const -> const Any& { + return *obj; + } + + //! Return the v-table pointer. + auto vptr() const -> vptr_type { + return detail::unbox_vptr(vp); + } + +#ifndef __MRDOCS__ + // Constrained to exactly this `virtual_any_ref`, for the same reason + // as in `virtual_any`: MSVC, in its default (permissive) mode, + // injects friend functions into the enclosing namespace, where an + // unconstrained parameter would make this a candidate for anything + // convertible to `virtual_any_ref`. + template + friend auto boost_openmethod_vptr(const Self& va, Registry*) + -> std::enable_if_t, vptr_type> { + return detail::unbox_vptr(va.vp); + } +#endif +}; + +//! Specialize virtual_traits for `virtual_any_ref`, passed by value. +//! +//! Dispatch is on the v-table pointer stored in the `virtual_any_ref`. +//! +//! @tparam Any An `any` type, possibly const-qualified. +//! @tparam Registry A @ref registry. +template +struct virtual_traits, Registry> { + //! The type used for dispatch. + using virtual_type = std::remove_const_t; + + //! Returns a const reference to the `virtual_any_ref` argument. + //! @param arg A reference to a `virtual_any_ref`. + //! @return A const reference to `arg`. + static auto peek(const virtual_any_ref& arg) + -> const virtual_any_ref& { + return arg; + } + + //! Cast to a type. + //! + //! If `U` is the `virtual_any_ref` itself, returns a copy of the + //! handle. Otherwise, extracts the referent's value using the + //! `virtual_traits` for the `any`'s reference type: mutable + //! references (e.g. `Dog&`) are supported unless `Any` is + //! const-qualified. + //! + //! @tparam U The target type (e.g. `Dog&`, `const Dog&`, `Dog`). + //! @param arg The `virtual_any_ref` method argument. + //! @return The value referred to by `arg`, cast to `U`. + template + static auto cast(virtual_any_ref arg) -> decltype(auto) { + if constexpr (std::is_same_v< + std::remove_cv_t>, + virtual_any_ref>) { + // by value: a reference would dangle when this function's + // parameter goes out of scope + return arg; + } else if constexpr (std::is_const_v) { + return virtual_traits::template cast< + U>(*arg.obj); + } else { + return virtual_traits::template cast( + *arg.obj); + } + } +}; + +namespace detail { + +template +struct is_virtual> : std::true_type {}; + +template +struct parameter_traits, Registry> + : virtual_traits, Registry> {}; + +template +struct validate_method_parameter< + virtual_any_ref, MethodRegistry, void> + : std::bool_constant> { + static_assert( + std::is_same_v, "registry mismatch"); +}; + +template +struct validate_method_parameter< + virtual_any_ref&, MethodRegistry, void> : std::false_type { + static_assert( + false_t, + "virtual_any_ref is a cheap handle, pass it by value"); +}; + +template +struct validate_method_parameter< + const virtual_any_ref&, MethodRegistry, void> + : std::false_type { + static_assert( + false_t, + "virtual_any_ref is a cheap handle, pass it by value"); +}; + +template +struct validate_method_parameter< + virtual_any_ref&&, MethodRegistry, void> : std::false_type { + static_assert( + false_t, + "virtual_any_ref is a cheap handle, pass it by value"); +}; + +template +struct validate_overrider_parameter, T2, void> + : std::true_type {}; + +template +struct validate_overrider_parameter< + virtual_any_ref, virtual_any_ref, void> + : std::true_type {}; + +template +struct select_overrider_virtual_type_aux< + virtual_any_ref, Q, Registry> { + using type = virtual_type; +}; + +} // namespace detail + namespace aliases { using boost::openmethod::virtual_any; +using boost::openmethod::virtual_any_ref; } // namespace aliases } // namespace boost::openmethod diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a47bce34..0edda63c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -165,6 +165,9 @@ openmethod_compile_fail_test( compile_fail_boost_any_mutable_ref_to_rvalue_ref "no matching") openmethod_compile_fail_test( compile_fail_virtual_any_by_value "virtual_any must be passed by reference") +openmethod_compile_fail_test( + compile_fail_virtual_any_ref_by_ref + "virtual_any_ref is a cheap handle, pass it by value") # "use of a deleted function" on gcc, "call to deleted function" on clang, # "attempting to reference a deleted function" on MSVC. openmethod_compile_fail_test( diff --git a/test/compile_fail_virtual_any_ref_by_ref.cpp b/test/compile_fail_virtual_any_ref_by_ref.cpp new file mode 100644 index 00000000..492e5996 --- /dev/null +++ b/test/compile_fail_virtual_any_ref_by_ref.cpp @@ -0,0 +1,27 @@ +// Copyright (c) 2018-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +#include + +#include +#include + +using namespace boost::openmethod; + +struct Dog { + std::string name; +}; + +BOOST_OPENMETHOD_REGISTER(use_std_any_types); + +// A virtual_any_ref method parameter is passed by value: it is a cheap, +// two-word handle; a reference would add an indirection for nothing. +BOOST_OPENMETHOD(name, (const virtual_any_ref&), std::string); + +int main() { + std::any dog(Dog{"Snoopy"}); + return name(virtual_any_ref(dog)).size(); +} diff --git a/test/test_virtual_any_ref.cpp b/test/test_virtual_any_ref.cpp new file mode 100644 index 00000000..ac72afbd --- /dev/null +++ b/test/test_virtual_any_ref.cpp @@ -0,0 +1,177 @@ +// Copyright (c) 2018-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +#include +#include + +#include +#include +#include + +#define BOOST_TEST_MODULE openmethod +#include + +using namespace boost::openmethod; + +#define MAKE_CLASSES() \ + struct Dog { \ + std::string name; \ + }; \ + \ + use_std_any_types BOOST_OPENMETHOD_GENSYM; + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// const handle: virtual_any_ref + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (virtual_any_ref), std::string); + +// A plain value does not convert to a virtual_any_ref, so +// BOOST_OPENMETHOD_OVERRIDE cannot locate the method for overriders that +// take the contained value. Register them with the core API instead - the +// primitive the macro itself expands to. + +using name_method = + BOOST_OPENMETHOD_TYPE(name, (virtual_any_ref), std::string); + +auto name_dog(const Dog& dog) -> std::string { + return dog.name + " the dog"; +} + +auto name_string(const std::string& name) -> std::string { + return name; +} + +BOOST_OPENMETHOD_REGISTER(name_method::override); +BOOST_OPENMETHOD_REGISTER(name_method::override); + +// The catch-all overrider takes the handle itself, by value; the macro +// locates the method, since the conversion is the identity. +BOOST_OPENMETHOD_OVERRIDE( + name, (virtual_any_ref va), std::string) { + return va.get().has_value() ? "something" : "nothing"; +} + +BOOST_AUTO_TEST_CASE(virtual_any_ref_const) { + initialize(trace()); + + // from an `any`: the v-table pointer is looked up from the dynamic + // type of the contained value + const std::any spot_any(Dog{"Spot"}); + virtual_any_ref spot = spot_any; + BOOST_TEST(spot.vptr() == default_registry::static_vptr); + BOOST_TEST(&spot.get() == &spot_any); + BOOST_TEST(name(spot) == "Spot the dog"); + + // an `any` lvalue converts to a (temporary) handle at the call site + std::any felix_any(std::string{"Felix the cat"}); + BOOST_TEST(name(felix_any) == "Felix the cat"); + + // from a virtual_any: the v-table pointer is copied - no lookup + const virtual_std_any rex = Dog{"Rex"}; + virtual_any_ref rex_ref = rex; + BOOST_TEST(rex_ref.vptr() == rex.vptr()); + BOOST_TEST(name(rex_ref) == "Rex the dog"); + + // a mutable handle converts to a const one + std::any answer_any(42); + virtual_any_ref answer = answer_any; + virtual_any_ref const_answer = answer; + BOOST_TEST(const_answer.vptr() == answer.vptr()); + + // `int` is registered, but has no specific overrider: the catch-all, + // registered for the `std::any` root, applies + BOOST_TEST(name(const_answer) == "something"); + + // copying a handle copies the two words; both refer to the same `any` + auto copy = spot; + BOOST_TEST(©.get() == &spot_any); + BOOST_TEST(copy.vptr() == spot.vptr()); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// mutable handle: virtual_any_ref + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(bump, (virtual_any_ref), std::string); + +using bump_method = + BOOST_OPENMETHOD_TYPE(bump, (virtual_any_ref), std::string); + +auto bump_dog(Dog& dog) -> std::string { + dog.name += " Jr."; + return dog.name + " the dog"; +} + +auto bump_int(int& value) -> std::string { + ++value; + return "bumped"; +} + +BOOST_OPENMETHOD_REGISTER(bump_method::override); +BOOST_OPENMETHOD_REGISTER(bump_method::override); + +BOOST_AUTO_TEST_CASE(virtual_any_ref_mutable) { + initialize(trace()); + + // the handle borrows the `any`; mutations reach the referent + std::any spot_any(Dog{"Spot"}); + BOOST_TEST(bump(spot_any) == "Spot Jr. the dog"); + BOOST_TEST(std::any_cast(spot_any).name == "Spot Jr."); + + std::any answer_any(41); + virtual_any_ref answer = answer_any; + BOOST_TEST(bump(answer) == "bumped"); + BOOST_TEST(std::any_cast(answer_any) == 42); + + // borrowing from a virtual_any: mutations reach the owner's value + virtual_std_any rex = Dog{"Rex"}; + virtual_any_ref rex_ref = rex; + BOOST_TEST(rex_ref.vptr() == rex.vptr()); + BOOST_TEST(bump(rex_ref) == "Rex Jr. the dog"); + BOOST_TEST(std::any_cast(rex.get()).name == "Rex Jr."); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// indirect vptrs + +struct Dog { + std::string name; +}; + +use_std_any_types + BOOST_OPENMETHOD_GENSYM; + +using name_method = method< + struct name_id, + std::string(virtual_any_ref), + indirect_registry>; + +auto name_dog(const Dog& dog) -> std::string { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_REGISTER(name_method::override); + +BOOST_AUTO_TEST_CASE(virtual_any_ref_indirect_vptr) { + initialize(); + + std::any spot_any(Dog{"Spot"}); + virtual_any_ref spot = spot_any; + BOOST_TEST(spot.vptr() == indirect_registry::static_vptr); + BOOST_TEST(name_method::fn(spot) == "Spot the dog"); +} +} // namespace BOOST_OPENMETHOD_GENSYM From c9807ab82337269ec34cdcaf9d93c1b2f0451f42 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 17:13:52 -0400 Subject: [PATCH 42/64] doc: stop advertising virtual_any for type_erasure anys Where the Concept can be edited, the openmethod_vptr concept strictly dominates virtual_any: constant time for every any of the type, in every flavor, with no wrapper and no per-object cost. Drop the virtual_any section from the TypeErasure page and the corresponding tests; the generic machinery still works for Concepts that cannot be modified. Co-Authored-By: Claude Fable 5 --- .../ROOT/pages/interop_type_erasure.adoc | 34 ++-------------- test/test_dispatch_type_erasure.cpp | 39 ------------------- 2 files changed, 4 insertions(+), 69 deletions(-) diff --git a/doc/modules/ROOT/pages/interop_type_erasure.adoc b/doc/modules/ROOT/pages/interop_type_erasure.adoc index 4e374e47..b722fd63 100644 --- a/doc/modules/ROOT/pages/interop_type_erasure.adoc +++ b/doc/modules/ROOT/pages/interop_type_erasure.adoc @@ -96,38 +96,16 @@ C++ RTTI dynamic type of the referent. The rvalue-reference flavor (`any`), and placeholders other than `_self`, are not supported. -#### `virtual_any` - -Every call above looks the v-table up in a hash table, keyed on the type the -`any` binds. cpp:virtual_any[] works for a `type_erasure::any` exactly as it -does for a `std::any`: `virtual_any>` bundles the `any` with the -v-table pointer for the value inside it, acquiring it once, on construction - -or not at all, when it is built from a value, since the type is then known at -compile time: - -```c++ -BOOST_OPENMETHOD(name, (const virtual_any&), std::string); - -// from an `any`: one lookup, at construction -virtual_any spot = erased(Dog{"Spot"}); - -// from a value: no lookup at all -virtual_any rex = Dog{"Rex"}; -``` - -The Concept must contain `relaxed` - `virtual_any`'s default constructor and -assignment rely on it - and `copy_constructible<>`, for copies. - For the same reason as for `std::any`, cpp:final_virtual_ptr[] is _deleted_ for `type_erasure::any`: it would silently produce the v-table of the root class rather than the one for the bound value. #### The `openmethod_vptr` concept -`virtual_any` removes the hash lookup by making the _object_ wider. -Boost.TypeErasure offers a way to remove it for plain, unwidened ``any``s: -since the `any` already carries a dispatch table of Concept operations, the -v-table pointer can be one of them. cpp:openmethod_vptr[] is a +Every call above looks the v-table up in a hash table, keyed on the type the +`any` binds. Boost.TypeErasure offers a way to remove that cost: since the +`any` already carries a dispatch table of Concept operations, the v-table +pointer can be one of them. cpp:openmethod_vptr[] is a Boost.TypeErasure concept that does exactly that. Include it in the Concept, and every flavor of the `any` gains an operation that returns the registry's static v-table pointer (`registry::static_vptr`) for the bound type - @@ -154,10 +132,6 @@ works with any pre-existing Concept containing `typeid_<>`. To use an `any` with several registries, list the concept several times, once per registry: `openmethod_vptr`. -An `any` that carries the concept cannot be wrapped in a `virtual_any` - and -does not need to be: both fill the same goal, constant-time access to the -v-table pointer. Wrapping one is rejected at compile time. - This concept is based on a design contributed by Steven Watanabe in link:https://github.com/boostorg/openmethod/issues/21[issue #21]. diff --git a/test/test_dispatch_type_erasure.cpp b/test/test_dispatch_type_erasure.cpp index 29929ee0..1dcfd25b 100644 --- a/test/test_dispatch_type_erasure.cpp +++ b/test/test_dispatch_type_erasure.cpp @@ -224,41 +224,6 @@ BOOST_AUTO_TEST_CASE(type_erasure_cref_wrapper_by_value) { namespace BOOST_OPENMETHOD_GENSYM { -// ----------------------------------------------------------------------------- -// virtual_any over a type_erasure any: the v-table pointer is looked up -// once, at construction - or set statically when the contained type is -// known - and dispatch does not hash typeid_of on every call - -MAKE_CLASSES(); - -BOOST_OPENMETHOD(name, (const virtual_any&), std::string); - -BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { - return dog.name + " the dog"; -} - -BOOST_AUTO_TEST_CASE(type_erasure_virtual_any) { - initialize(trace()); - - // from an `any`: runtime lookup via typeid_of - erased spot_any(Dog{"Spot"}); - virtual_any spot = spot_any; - BOOST_TEST(spot.vptr() == default_registry::static_vptr); - BOOST_TEST(name(spot) == "Spot the dog"); - - // from a value: the v-table pointer is set statically - virtual_any rex = Dog{"Rex"}; - BOOST_TEST(rex.vptr() == default_registry::static_vptr); - BOOST_TEST(name(rex) == "Rex the dog"); - - auto snoopy = make_any_virtual(Dog{"Snoopy"}); - BOOST_TEST(snoopy.vptr() == default_registry::static_vptr); - BOOST_TEST(name(snoopy) == "Snoopy the dog"); -} -} // namespace BOOST_OPENMETHOD_GENSYM - -namespace BOOST_OPENMETHOD_GENSYM { - // ----------------------------------------------------------------------------- // indirect vptrs @@ -283,10 +248,6 @@ BOOST_AUTO_TEST_CASE(type_erasure_indirect_vptr) { const erased spot(Dog{"Spot"}); BOOST_TEST(name_method::fn(spot) == "Spot the dog"); - - virtual_any rex = Dog{"Rex"}; - BOOST_TEST(rex.vptr() == indirect_registry::static_vptr); - BOOST_TEST(name_method::fn(rex.get()) == "Rex the dog"); } } // namespace BOOST_OPENMETHOD_GENSYM From cb44af63e6038a42fa3e045bd9ae048979b15fe9 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 20:04:40 -0400 Subject: [PATCH 43/64] test: exercise virtual_any_ref with boost::any Co-Authored-By: Claude Fable 5 --- test/test_virtual_any_ref.cpp | 61 +++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/test/test_virtual_any_ref.cpp b/test/test_virtual_any_ref.cpp index ac72afbd..70967ee2 100644 --- a/test/test_virtual_any_ref.cpp +++ b/test/test_virtual_any_ref.cpp @@ -7,7 +7,9 @@ #include #include +#include #include +#include #include #include @@ -145,6 +147,65 @@ BOOST_AUTO_TEST_CASE(virtual_any_ref_mutable) { namespace BOOST_OPENMETHOD_GENSYM { +// ----------------------------------------------------------------------------- +// boost::any: virtual_any_ref is generic over the `any` type + +struct Dog { + std::string name; +}; + +use_boost_any_types BOOST_OPENMETHOD_GENSYM; + +BOOST_OPENMETHOD(name, (virtual_any_ref), std::string); + +using name_method = BOOST_OPENMETHOD_TYPE( + name, (virtual_any_ref), std::string); + +auto name_dog(const Dog& dog) -> std::string { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_REGISTER(name_method::override); + +BOOST_OPENMETHOD_OVERRIDE( + name, (virtual_any_ref va), std::string) { + return va.get().empty() ? "nothing" : "something"; +} + +BOOST_OPENMETHOD(bump, (virtual_any_ref), std::string); + +using bump_method = + BOOST_OPENMETHOD_TYPE(bump, (virtual_any_ref), std::string); + +auto bump_dog(Dog& dog) -> std::string { + dog.name += " Jr."; + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_REGISTER(bump_method::override); + +BOOST_AUTO_TEST_CASE(virtual_any_ref_boost_any) { + initialize(trace()); + + const boost::any spot_any(Dog{"Spot"}); + virtual_any_ref spot = spot_any; + BOOST_TEST(spot.vptr() == default_registry::static_vptr); + BOOST_TEST(name(spot) == "Spot the dog"); + + // `int` is registered, but has no specific overrider: the catch-all, + // registered for the `boost::any` root, applies + boost::any answer_any(42); + BOOST_TEST(name(answer_any) == "something"); + + // mutations through a mutable handle reach the referent + boost::any rex_any(Dog{"Rex"}); + BOOST_TEST(bump(rex_any) == "Rex Jr. the dog"); + BOOST_TEST(boost::any_cast(rex_any).name == "Rex Jr."); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + // ----------------------------------------------------------------------------- // indirect vptrs From 9221d1501e01a58fa64b00a20c6926708998cb70 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Mon, 10 Aug 2026 01:44:33 -0400 Subject: [PATCH 44/64] doc: point the `any` header links at GitHub too Co-Authored-By: Claude Opus 5 --- doc/modules/ROOT/pages/ref_headers.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/modules/ROOT/pages/ref_headers.adoc b/doc/modules/ROOT/pages/ref_headers.adoc index b01c59d6..07309b7a 100644 --- a/doc/modules/ROOT/pages/ref_headers.adoc +++ b/doc/modules/ROOT/pages/ref_headers.adoc @@ -79,7 +79,7 @@ Provides a `virtual_traits` specialization that makes it possible to use a `boost::intrusive_ptr` in place of a raw pointer or reference in virtual parameters. [#virtual_any] -### link:../../../include/boost/openmethod/interop/virtual_any.hpp[] +### link:{headers-url}/boost/openmethod/interop/virtual_any.hpp[] Provides `virtual_any`, a wide `any` that combines an `any`, held by value, with a pointer to the v-table for the contained value - similar to `virtual_ptr`. @@ -87,13 +87,13 @@ Also provides `virtual_any_ref`, a non-owning counterpart that borrows an existing `any`. [#std_any] -### link:../../../include/boost/openmethod/interop/std_any.hpp[] +### link:{headers-url}/boost/openmethod/interop/std_any.hpp[] Provides `virtual_traits` specializations that make it possible to use a `std::any` in virtual parameters. [#boost_any] -### link:../../../include/boost/openmethod/interop/boost_any.hpp[] +### link:{headers-url}/boost/openmethod/interop/boost_any.hpp[] Provides `virtual_traits` specializations that make it possible to use a `boost::any` in virtual parameters. From 32e064c33d44de9eab209ea031d16794acb30fbb Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Mon, 10 Aug 2026 01:44:44 -0400 Subject: [PATCH 45/64] doc: point the type_erasure header link at GitHub too Co-Authored-By: Claude Opus 5 --- doc/modules/ROOT/pages/ref_headers.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/modules/ROOT/pages/ref_headers.adoc b/doc/modules/ROOT/pages/ref_headers.adoc index 1c7d9c7e..65edc610 100644 --- a/doc/modules/ROOT/pages/ref_headers.adoc +++ b/doc/modules/ROOT/pages/ref_headers.adoc @@ -99,7 +99,7 @@ Provides `virtual_traits` specializations that make it possible to use a `boost::any` in virtual parameters. [#boost_type_erasure] -### link:../../../include/boost/openmethod/interop/boost_type_erasure.hpp[] +### link:{headers-url}/boost/openmethod/interop/boost_type_erasure.hpp[] Provides specializations for using a `boost::type_erasure::any` in virtual parameters. From a131c7f173eef7b4c4e5197d608e4a0714755341 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Mon, 10 Aug 2026 16:32:40 -0400 Subject: [PATCH 46/64] fix MSVC: constrain the type_erasure boost_openmethod_vptr friend MSVC's /std:c++17 does not imply /permissive-, so it injects hidden friends into the enclosing namespace, where ordinary lookup finds them. The unconstrained `const derived::type&` parameter then made the hook a candidate for an `any` over an unrelated Concept: MSVC tried to convert one flavor to the other, instantiating TypeErasure's binding converting constructor, which fails outside the immediate context -- an error, not a substitution failure. boost/type_erasure/any.hpp(2054): error C2661: 'binding::binding': no overloaded function takes 2 arguments Deduce the parameter and require an exact match, as virtual_any already does. Co-Authored-By: Claude Opus 5 --- .../openmethod/interop/boost_type_erasure.hpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/include/boost/openmethod/interop/boost_type_erasure.hpp b/include/boost/openmethod/interop/boost_type_erasure.hpp index bfe8abbc..6345e226 100644 --- a/include/boost/openmethod/interop/boost_type_erasure.hpp +++ b/include/boost/openmethod/interop/boost_type_erasure.hpp @@ -567,9 +567,19 @@ namespace boost::type_erasure { template struct concept_interface< boost::openmethod::openmethod_vptr, Base, T> : Base { - friend auto boost_openmethod_vptr( - const typename derived::type& arg, - Registry*) -> boost::openmethod::vptr_type { + // The parameter is a deduced `Self`, constrained to the exact any + // flavor, because MSVC's `/std:c++17` does not imply `/permissive-`: + // it injects hidden friends into the enclosing namespace, where + // ordinary lookup finds them. A `const derived::type&` + // parameter would then make this a candidate for an any over an + // unrelated Concept, which MSVC tries to convert to this one - and + // the conversion fails outside the immediate context, so it is an + // error, not a substitution failure. + template + friend auto boost_openmethod_vptr(const Self& arg, Registry*) + -> std::enable_if_t< + std::is_same_v::type>, + boost::openmethod::vptr_type> { return call( boost::openmethod::openmethod_vptr(), arg); } From 47c80a855b437678f0027ab43aab92c635d95bd4 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Mon, 10 Aug 2026 16:36:43 -0400 Subject: [PATCH 47/64] test: make the type_erasure by-value guard fire on every compiler The test declared the method but never called it. GCC, clang and msvc-14.3 instantiate `method<...>` at the declaration, so the guard fired; MSVC v18 defers, and the test compiled clean - a compile-fail test that no longer fails. Call the method, as the virtual_any by-value test does. Co-Authored-By: Claude Opus 5 --- test/compile_fail_type_erasure_by_value.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/compile_fail_type_erasure_by_value.cpp b/test/compile_fail_type_erasure_by_value.cpp index 9efe2b26..c1c498c9 100644 --- a/test/compile_fail_type_erasure_by_value.cpp +++ b/test/compile_fail_type_erasure_by_value.cpp @@ -30,5 +30,8 @@ BOOST_OPENMETHOD_REGISTER(use_type_erasure_types); BOOST_OPENMETHOD(name, (virtual_), std::string); int main() { - return 0; + // Call the method: declaring it is not enough to instantiate it on + // every compiler, and the guard lives in the method's body. + erased dog = Dog{"Snoopy"}; + return name(dog).size(); } From f3440dbc6699608ede814cf4f81c87651f798a25 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Tue, 11 Aug 2026 20:10:29 -0400 Subject: [PATCH 48/64] ci: split the macOS 10.15 Drone stages by C++ standard The Catalina runner is the slowest in the fleet: at the observed rate the UBSAN and ASAN stages needed ~105 minutes for 17,2a, and both were killed (exit 137) at Drone's 60-minute step timeout partway into the second standard. Give each standard its own stage, as the GCC 13 Linux stages already do for the same reason. Co-Authored-By: Claude Opus 5 --- .drone.jsonnet | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index c4d88b0a..8acdabdd 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -297,14 +297,27 @@ local windows_pipeline(name, image, environment, arch = "amd64") = "clang-18", ), + # One C++ standard per stage: the Catalina runner is the slowest in the + # fleet, and 17,2a in a single stage was killed at Drone's 60-minute step + # timeout, halfway into the second standard. macos_pipeline( - "MacOS 10.15 Xcode 12.2 UBSAN", - { TOOLSET: 'clang', COMPILER: 'clang++', CXXSTD: '17,2a' } + ubsan, + "MacOS 10.15 Xcode 12.2 UBSAN C++17", + { TOOLSET: 'clang', COMPILER: 'clang++', CXXSTD: '17' } + ubsan, ), macos_pipeline( - "MacOS 10.15 Xcode 12.2 ASAN", - { TOOLSET: 'clang', COMPILER: 'clang++', CXXSTD: '17,2a' } + asan, + "MacOS 10.15 Xcode 12.2 UBSAN C++2a", + { TOOLSET: 'clang', COMPILER: 'clang++', CXXSTD: '2a' } + ubsan, + ), + + macos_pipeline( + "MacOS 10.15 Xcode 12.2 ASAN C++17", + { TOOLSET: 'clang', COMPILER: 'clang++', CXXSTD: '17' } + asan, + ), + + macos_pipeline( + "MacOS 10.15 Xcode 12.2 ASAN C++2a", + { TOOLSET: 'clang', COMPILER: 'clang++', CXXSTD: '2a' } + asan, ), macos_pipeline( From 178845753cc5c4e8f46cdd615900f294c6897051 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 15 Aug 2026 10:23:19 -0400 Subject: [PATCH 49/64] doc, test: cover virtual_any with type_erasure::any virtual_any over a boost::type_erasure::any was supported - the header lists the form among the supported virtual parameters - but had neither a test nor documentation. Add test/test_virtual_any_type_erasure.cpp: the three reference categories, value semantics, virtual_any_ref (also previously uncovered), and indirect vptrs. Document virtual_any in interop_type_erasure.adoc, in a new subsection before the openmethod_vptr one, with a new example. Open the openmethod_vptr section by presenting the concept as the alternative: same goal, reached from the other side, and mutually exclusive with virtual_any. Fix a silent AsciiDoc bug while there: `word`'s is parsed as a code span followed by the `' curly-apostrophe shorthand, which eats the closing backtick, so the next code span in the paragraph loses its formatting and a literal backtick reaches the output. Reword the two pre-existing occurrences, and record the trap in CLAUDE.md. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 33 ++ .../examples/type_erasure_virtual_any.cpp | 60 ++++ .../ROOT/pages/interop_type_erasure.adoc | 54 ++- doc/modules/ROOT/pages/shared_libraries.adoc | 5 +- test/test_virtual_any_type_erasure.cpp | 322 ++++++++++++++++++ 5 files changed, 462 insertions(+), 12 deletions(-) create mode 100644 doc/modules/ROOT/examples/type_erasure_virtual_any.cpp create mode 100644 test/test_virtual_any_type_erasure.cpp diff --git a/CLAUDE.md b/CLAUDE.md index 0bb72468..3beda037 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -159,6 +159,39 @@ Tests require these C++17 features (checked by Boost.Build): - structured bindings - ``, ``, `` headers +### Documentation (AsciiDoc) + +Prose lives in `doc/modules/ROOT/pages/*.adoc`; explanations belong there, not in comments inside +the example sources under `doc/modules/ROOT/examples/`, which are pulled into the rendered page +verbatim through `include::example$file.cpp[tag=content]`. Pages hard-wrap at ~79 columns and use +`cpp:name[]` for API names that have a reference page. + +**Render the docs; do not just eyeball the `.adoc`.** `doc/build_antora.sh` (~2 min, writes the +gitignored `doc/html/`) is the only way to catch markup that is silently mis-parsed — asciidoctor +emits no warning for it. + +**The backtick-apostrophe trap**: never write a possessive right after a code span. Asciidoctor +parses ``` `any`'s ``` as `` ` `` + `any` + the **`` `' `` curly-apostrophe shorthand**, which +consumes the *closing* backtick; the opening one is then left unmatched and pairs with the next +backtick in the same paragraph. Two things break at once — a literal `` ` `` appears in the output, +and the following code span loses its `` formatting: + +``` +source: is part of the `any`'s type - whereas the `typeid_of`-based dispatch above +rendered: is part of the any's type - whereas the `typeid_of-based dispatch above +``` + +Reword instead: "the reference types of the `any`", "separate from that of `default_registry`". +``{apos}`` also works and matches the house style (`shared_libraries.adoc` uses ``{empty}`` for +plurals: ``` `virtual_ptr`{empty}s ```), but rewording is safer and reads better. Before building: + +```bash +grep -rn "\`'" doc/modules/ROOT/pages/*.adoc # must return nothing +``` + +After building, no stray backticks should survive outside code blocks — +`grep -n '\`' doc/html/openmethod/.html` should only hit backticks inside C++ comments. + ## Common Development Patterns ### Working with Shared Libraries / DLL Support diff --git a/doc/modules/ROOT/examples/type_erasure_virtual_any.cpp b/doc/modules/ROOT/examples/type_erasure_virtual_any.cpp new file mode 100644 index 00000000..ddd71e2d --- /dev/null +++ b/doc/modules/ROOT/examples/type_erasure_virtual_any.cpp @@ -0,0 +1,60 @@ +// Copyright (c) 2018-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) + +// clang-format off + +// tag::content[] +#include +#include + +#include +#include +#include + +#include +#include + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +using Concept = boost::mpl::vector, te::relaxed>; +using erased = te::any; + +struct Dog { + std::string name; +}; + +BOOST_OPENMETHOD_REGISTER(use_type_erasure_types); + +BOOST_OPENMETHOD(name, (const virtual_any&), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { + return name; +} + +#include + +int main() { + initialize(); + + // from a value: the v-table pointer is set statically + virtual_any rex = Dog{"Rex"}; + std::cout << (rex.vptr() == default_registry::static_vptr) << "\n"; // 1 + + // from an `any`: one lookup, here, and none in the calls below + erased spot_any(Dog{"Spot"}); + virtual_any spot = spot_any; + + std::cout << name(rex) << "\n"; // Rex the dog + std::cout << name(spot) << "\n"; // Spot the dog + + spot = std::string("Felix the cat"); + std::cout << name(spot) << "\n"; // Felix the cat +} +// end::content[] diff --git a/doc/modules/ROOT/pages/interop_type_erasure.adoc b/doc/modules/ROOT/pages/interop_type_erasure.adoc index b722fd63..13b3ce0f 100644 --- a/doc/modules/ROOT/pages/interop_type_erasure.adoc +++ b/doc/modules/ROOT/pages/interop_type_erasure.adoc @@ -100,18 +100,52 @@ For the same reason as for `std::any`, cpp:final_virtual_ptr[] is _deleted_ for `type_erasure::any`: it would silently produce the v-table of the root class rather than the one for the bound value. -#### The `openmethod_vptr` concept +#### `virtual_any` Every call above looks the v-table up in a hash table, keyed on the type the -`any` binds. Boost.TypeErasure offers a way to remove that cost: since the -`any` already carries a dispatch table of Concept operations, the v-table -pointer can be one of them. cpp:openmethod_vptr[] is a -Boost.TypeErasure concept that does exactly that. Include it in the Concept, -and every flavor of the `any` gains an operation that returns the registry's +`any` binds. cpp:virtual_any[] removes that cost: it bundles an `any` with the +v-table pointer for the value bound to it, acquiring it once, on construction, +and maintaining it across assignment and `emplace`. It is to an `any` what +cpp:virtual_ptr[] is to a pointer, except that it _owns_ the object: the `any` +is held by value. The Concept needs `relaxed` - for the default constructor +and assignment - and `copy_constructible<>` for copies. + +[source,c++] +---- +include::example$type_erasure_virtual_any.cpp[tag=content] +---- + +The wrapper is passed by reference, in any of the three categories, and the +overriders receive the _bound_ value, exactly as for `virtual_&>` - or the wrapper itself, for a catch-all overrider. +cpp:virtual_any_ref[] is the non-owning counterpart: it _borrows_ an existing +`any`, looking the v-table pointer up once, or taking it at no cost from a +`virtual_any`. Both work with `type_erasure::any` for the same reason they +work with `std::any` and `boost::any` - the interop specializes +cpp:virtual_traits[] for the reference types of the `any`. See +xref:interop_any.adoc[Interoperation with `any`] for the details. + +#### The `openmethod_vptr` concept + +cpp:openmethod_vptr[] is an alternative to `virtual_any`, pursuing the same +goal - constant-time access to the v-table pointer - from the other side. +Instead of wrapping the `any` from the outside, it puts the v-table pointer +inside the dispatch table of the `any` itself, making every flavor of it +intrinsically polymorphic, and leaving the call sites unchanged. The two are +mutually exclusive: an `any` that carries the concept cannot be wrapped in a +`virtual_any` - and does not need to be; wrapping one is rejected at compile +time. `virtual_any` is to an `any` what cpp:virtual_ptr[] is to a pointer, +while `openmethod_vptr` is to an `any` what cpp:inplace_vptr_base[] is to a +class - see xref:virtual_ptr_alt.adoc[Alternatives to virtual_ptr]. + +The `any` already carries a dispatch table of Concept operations, so the +v-table pointer can be one of them. Include it in the Concept, and every +flavor of the `any` gains an operation that returns the registry's static v-table pointer (`registry::static_vptr`) for the bound type - -instantiated once per bound type, retrieved in constant time. The concept surfaces the operation as a -`boost_openmethod_vptr` overload, which dispatch prefers over the `vptr` -policy, so calls through such an `any` never hash the result of `typeid_of`. +instantiated once per bound type, retrieved in constant time. The concept +surfaces the operation as a `boost_openmethod_vptr` overload, which dispatch +prefers over the `vptr` policy, so calls through such an `any` never hash the +result of `typeid_of`. Binding a value to such an `any` also _registers_ its type, as a class derived from the owning flavor - the registrar is instantiated along with the @@ -127,7 +161,7 @@ include::example$type_erasure_concept.cpp[tag=content] ---- The price is coupling: the Concept must be OpenMethod-aware, and the registry -is part of the `any`'s type - whereas the `typeid_of`-based dispatch above +is part of the type of the `any` - whereas the `typeid_of`-based dispatch above works with any pre-existing Concept containing `typeid_<>`. To use an `any` with several registries, list the concept several times, once per registry: `openmethod_vptr`. diff --git a/doc/modules/ROOT/pages/shared_libraries.adoc b/doc/modules/ROOT/pages/shared_libraries.adoc index 97da9bbf..0861c919 100644 --- a/doc/modules/ROOT/pages/shared_libraries.adoc +++ b/doc/modules/ROOT/pages/shared_libraries.adoc @@ -253,8 +253,9 @@ and no translation unit can accidentally disagree: include::{shared}/indirect_vptr/animals.hpp[tag=content] ---- -cpp:indirect_registry[] has its own state, separate from `default_registry`'s, -shared with the same three macros: just name `indirect_registry`, as above. +cpp:indirect_registry[] has its own state, separate from that of +`default_registry`, shared with the same three macros: just name +`indirect_registry`, as above. Here is a program that carries `virtual_ptr`{empty}s across `initialize` calls. It owns the state, so it defines `OWNS_REGISTRY_STATE` and emits the definition: diff --git a/test/test_virtual_any_type_erasure.cpp b/test/test_virtual_any_type_erasure.cpp new file mode 100644 index 00000000..4a0b004a --- /dev/null +++ b/test/test_virtual_any_type_erasure.cpp @@ -0,0 +1,322 @@ +// Copyright (c) 2018-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#define BOOST_TEST_MODULE openmethod +#include + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +// `relaxed` is needed for virtual_any's default constructor and +// assignment, `copy_constructible<>` for its copies. `relaxed` also +// implies typeid_<>, on which the v-table lookup relies. +using Concept = boost::mpl::vector, te::relaxed>; +using erased = te::any; +using virtual_erased = virtual_any; + +#define MAKE_CLASSES() \ + struct Dog { \ + std::string name; \ + }; \ + \ + BOOST_OPENMETHOD_REGISTER( \ + use_type_erasure_types); + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as const virtual_any& (const ref) + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (const virtual_erased&), std::string); + +// The overriders can use the macro: the value constructor of virtual_any +// makes the overrider's parameter convertible to the method's, so the +// method is located. + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { + return name; +} + +// A catch-all overrider may keep the wrapper. +BOOST_OPENMETHOD_OVERRIDE(name, (const virtual_erased& va), std::string) { + return !te::is_empty(va.get()) ? "something" : "nothing"; +} + +BOOST_AUTO_TEST_CASE(virtual_any_by_const_ref) { + initialize(trace()); + + // from an `any`: the v-table pointer is looked up from the type bound + // to the `any` + const erased spot_any(Dog{"Spot"}); + virtual_erased spot = spot_any; + BOOST_TEST(spot.vptr() == default_registry::static_vptr); + BOOST_TEST(name(spot) == "Spot the dog"); + + // from a value: the v-table pointer is set statically + virtual_erased rex = Dog{"Rex"}; + BOOST_TEST(rex.vptr() == default_registry::static_vptr); + BOOST_TEST(name(rex) == "Rex the dog"); + + virtual_erased felix = std::string("Felix the cat"); + BOOST_TEST(felix.vptr() == default_registry::static_vptr); + BOOST_TEST(name(felix) == "Felix the cat"); + + // a value converts to a (temporary) virtual_any at the call site + BOOST_TEST(name(Dog{"Fido"}) == "Fido the dog"); + + // `int` is registered, but has no specific overrider: the catch-all, + // registered for the owning flavor, applies + BOOST_TEST(name(42) == "something"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as virtual_any& (mutable ref) + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(bump, (virtual_erased&), std::string); + +// BOOST_OPENMETHOD_OVERRIDE cannot express this: a temporary virtual_any +// binds to `const virtual_erased&` and to `virtual_erased&&`, but nothing +// binds to a mutable lvalue reference. Register directly via +// method<...>::override instead - the primitive the macro itself +// expands to. + +using bump_method = BOOST_OPENMETHOD_TYPE(bump, (virtual_erased&), std::string); + +auto bump_dog(Dog& dog) -> std::string { + dog.name += " Jr."; + return dog.name + " the dog"; +} + +auto bump_int(int& value) -> std::string { + ++value; + return "bumped"; +} + +BOOST_OPENMETHOD_REGISTER(bump_method::override); +BOOST_OPENMETHOD_REGISTER(bump_method::override); + +BOOST_AUTO_TEST_CASE(virtual_any_by_mutable_ref) { + initialize(trace()); + + virtual_erased spot = Dog{"Spot"}; + BOOST_TEST(bump(spot) == "Spot Jr. the dog"); + // the mutation is visible through the virtual_any + BOOST_TEST(te::any_cast(spot.get()).name == "Spot Jr."); + + virtual_erased answer = 41; + BOOST_TEST(bump(answer) == "bumped"); + BOOST_TEST(te::any_cast(answer.get()) == 42); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as virtual_any&& (xvalue ref) + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(steal, (virtual_erased&&), std::string); + +// boost::type_erasure::any_cast has no rvalue overload; the trait moves +// the result of a mutable-reference cast, because the `any` owns its +// value. +BOOST_OPENMETHOD_OVERRIDE(steal, (Dog && dog), std::string) { + Dog stolen(std::move(dog)); + return stolen.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(steal, (std::string && name), std::string) { + std::string stolen(std::move(name)); + return stolen; +} + +BOOST_AUTO_TEST_CASE(virtual_any_by_xvalue_ref) { + initialize(trace()); + + virtual_erased spot = Dog{"Spot"}; + BOOST_TEST(steal(std::move(spot)) == "Spot the dog"); + // the overrider moved the name out; the virtual_any still owns the Dog + BOOST_TEST(!te::is_empty(spot.get())); + BOOST_TEST(te::any_cast(spot.get()).name == ""); + + BOOST_TEST( + steal(virtual_erased(std::string("Felix the cat"))) == "Felix the cat"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// value semantics + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (const virtual_erased&), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_AUTO_TEST_CASE(virtual_any_value_semantics) { + initialize(trace()); + + virtual_erased empty; + BOOST_TEST(te::is_empty(empty.get())); + BOOST_TEST(empty.vptr() == nullptr); + + virtual_erased rex = Dog{"Rex"}; + + // copy: independent payloads, same v-table pointer + auto copy = rex; + BOOST_TEST(copy.vptr() == rex.vptr()); + BOOST_TEST(name(copy) == "Rex the dog"); + BOOST_TEST(name(rex) == "Rex the dog"); // original unaffected + + // move: the source's v-table pointer is nulled + auto moved = std::move(copy); + BOOST_TEST(moved.vptr() == default_registry::static_vptr); + BOOST_TEST(copy.vptr() == nullptr); + BOOST_TEST(name(moved) == "Rex the dog"); + + // assignment from an `any` re-derives the v-table pointer + erased felix_any(std::string{"Felix"}); + moved = felix_any; + BOOST_TEST(moved.vptr() == default_registry::static_vptr); + + // assignment from a value sets it statically + moved = Dog{"Snoopy"}; + BOOST_TEST(moved.vptr() == default_registry::static_vptr); + BOOST_TEST(name(moved) == "Snoopy the dog"); + + // emplace constructs in place and sets it statically + moved.emplace("Sylvester"); + BOOST_TEST(moved.vptr() == default_registry::static_vptr); + BOOST_TEST(te::any_cast(moved.get()) == "Sylvester"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// virtual_any_ref: the non-owning counterpart + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (virtual_any_ref), std::string); + +// A plain value does not convert to a virtual_any_ref, so +// BOOST_OPENMETHOD_OVERRIDE cannot locate the method for overriders that +// take the bound value. Register them with the core API instead. + +using name_method = + BOOST_OPENMETHOD_TYPE(name, (virtual_any_ref), std::string); + +auto name_dog(const Dog& dog) -> std::string { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_REGISTER(name_method::override); + +BOOST_OPENMETHOD(bump, (virtual_any_ref), std::string); + +using bump_method = + BOOST_OPENMETHOD_TYPE(bump, (virtual_any_ref), std::string); + +auto bump_dog(Dog& dog) -> std::string { + dog.name += " Jr."; + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_REGISTER(bump_method::override); + +BOOST_AUTO_TEST_CASE(virtual_any_ref_by_value) { + initialize(trace()); + + // from an `any`: the v-table pointer is looked up + const erased spot_any(Dog{"Spot"}); + virtual_any_ref spot = spot_any; + BOOST_TEST(spot.vptr() == default_registry::static_vptr); + BOOST_TEST(&spot.get() == &spot_any); + BOOST_TEST(name(spot) == "Spot the dog"); + + // an `any` lvalue converts to a (temporary) handle at the call site + erased fido_any(Dog{"Fido"}); + BOOST_TEST(name(fido_any) == "Fido the dog"); + + // from a virtual_any: the v-table pointer is copied - no lookup + virtual_erased rex = Dog{"Rex"}; + virtual_any_ref rex_cref = rex; + BOOST_TEST(rex_cref.vptr() == rex.vptr()); + BOOST_TEST(name(rex_cref) == "Rex the dog"); + + // through a mutable handle, mutations reach the owner's value + virtual_any_ref rex_ref = rex; + BOOST_TEST(bump(rex_ref) == "Rex Jr. the dog"); + BOOST_TEST(te::any_cast(rex.get()).name == "Rex Jr."); + + // a mutable handle converts to a const one + virtual_any_ref const_rex = rex_ref; + BOOST_TEST(const_rex.vptr() == rex_ref.vptr()); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// indirect vptrs + +struct Dog { + std::string name; +}; + +BOOST_OPENMETHOD_REGISTER( + use_type_erasure_types); + +using name_method = method< + struct name_id, std::string(const virtual_any&), + indirect_registry>; + +auto name_dog(const Dog& dog) -> std::string { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_REGISTER(name_method::override); + +BOOST_AUTO_TEST_CASE(virtual_any_indirect_vptr) { + initialize(); + + erased spot_any(Dog{"Spot"}); + virtual_any spot = spot_any; + BOOST_TEST(spot.vptr() == indirect_registry::static_vptr); + BOOST_TEST(name_method::fn(spot) == "Spot the dog"); + + virtual_any rex = Dog{"Rex"}; + BOOST_TEST(name_method::fn(rex) == "Rex the dog"); +} +} // namespace BOOST_OPENMETHOD_GENSYM From d57976647846fa0fe12206c546e33e9157d84e8c Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 15 Aug 2026 10:29:29 -0400 Subject: [PATCH 50/64] doc: give the acknowledgment its own section on both interop pages Move the Steven Watanabe credit out of the openmethod_vptr section, where it read as being about that concept alone, into an Acknowledgment section at the end of each interop page, covering the interop as a whole. Co-Authored-By: Claude Opus 5 --- doc/modules/ROOT/pages/interop_any.adoc | 4 ++++ doc/modules/ROOT/pages/interop_type_erasure.adoc | 7 ++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/doc/modules/ROOT/pages/interop_any.adoc b/doc/modules/ROOT/pages/interop_any.adoc index 1f89dfb6..101779ad 100644 --- a/doc/modules/ROOT/pages/interop_any.adoc +++ b/doc/modules/ROOT/pages/interop_any.adoc @@ -170,3 +170,7 @@ cpp:virtual_any[] itself is generic: it can serve any type with an `any`-like interface, given cpp:virtual_traits[] specializations for its reference types. Boost.TypeErasure's `any` is supported on the same model - see xref:interop_type_erasure.adoc[Interoperation with Boost.TypeErasure]. + +### Acknowledgment + +This interop is based on a design contributed by Steven Watanabe. diff --git a/doc/modules/ROOT/pages/interop_type_erasure.adoc b/doc/modules/ROOT/pages/interop_type_erasure.adoc index 13b3ce0f..35447c1f 100644 --- a/doc/modules/ROOT/pages/interop_type_erasure.adoc +++ b/doc/modules/ROOT/pages/interop_type_erasure.adoc @@ -166,9 +166,6 @@ works with any pre-existing Concept containing `typeid_<>`. To use an `any` with several registries, list the concept several times, once per registry: `openmethod_vptr`. -This concept is based on a design contributed by Steven Watanabe in -link:https://github.com/boostorg/openmethod/issues/21[issue #21]. - #### Empty ``any``s An empty relaxed `any` reports `typeid(void)`, which is not a registered @@ -177,3 +174,7 @@ overrider does not help: dispatch never reaches it. Check with `boost::type_erasure::is_empty` before calling. With the `openmethod_vptr` concept, the failure mode differs: calling a concept operation on an empty relaxed `any` throws `boost::type_erasure::bad_function_call`. + +### Acknowledgment + +This interop is based on a design contributed by Steven Watanabe. From 408fe46a8b708a33b72e259f144f12fe7145c2d8 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 15 Aug 2026 10:34:41 -0400 Subject: [PATCH 51/64] doc: credit the interop design in the headers, matching the pages Follow the Acknowledgment sections added to the interop pages: credit the interop as a whole rather than the openmethod_vptr concept alone. In boost_type_erasure.hpp, drop the parenthetical from the file comment and the credit from the openmethod_vptr doc comment - which rendered onto its reference page - and state it once, at the end of the file comment. Give std_any.hpp and boost_any.hpp, which had no file comment at all, the same line. virtual_any.hpp is left alone. Co-Authored-By: Claude Opus 5 --- include/boost/openmethod/interop/boost_any.hpp | 5 +++++ .../openmethod/interop/boost_type_erasure.hpp | 17 ++++++++--------- include/boost/openmethod/interop/std_any.hpp | 5 +++++ 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/include/boost/openmethod/interop/boost_any.hpp b/include/boost/openmethod/interop/boost_any.hpp index d65fc9f5..09920a65 100644 --- a/include/boost/openmethod/interop/boost_any.hpp +++ b/include/boost/openmethod/interop/boost_any.hpp @@ -10,6 +10,11 @@ #include #include +// Dispatch on the type contained in a `boost::any`. +// +// This interop is based on a design contributed by Steven Watanabe: +// https://github.com/boostorg/openmethod/issues/21 + namespace boost::openmethod { namespace detail { diff --git a/include/boost/openmethod/interop/boost_type_erasure.hpp b/include/boost/openmethod/interop/boost_type_erasure.hpp index 6345e226..71ab1368 100644 --- a/include/boost/openmethod/interop/boost_type_erasure.hpp +++ b/include/boost/openmethod/interop/boost_type_erasure.hpp @@ -45,12 +45,14 @@ // The rvalue-reference flavor (`any`), and placeholders // other than `_self`, are not supported. // -// In addition, `openmethod_vptr` - based on a design by Steven Watanabe - -// is a Boost.TypeErasure concept that stores the v-table pointer for the -// bound type in the any's own dispatch table, making every flavor of the -// any intrinsically polymorphic: calls resolve in constant time, without -// hashing the result of `typeid_of`, and binding a value to the any -// registers its type. +// In addition, `openmethod_vptr` is a Boost.TypeErasure concept that +// stores the v-table pointer for the bound type in the any's own dispatch +// table, making every flavor of the any intrinsically polymorphic: calls +// resolve in constant time, without hashing the result of `typeid_of`, +// and binding a value to the any registers its type. +// +// This interop is based on a design contributed by Steven Watanabe: +// https://github.com/boostorg/openmethod/issues/21 namespace boost::openmethod { @@ -528,9 +530,6 @@ use_class_aux>> //! constant-time access to the v-table pointer. Wrapping one is rejected //! at compile time. //! -//! Based on a design by -//! [Steven Watanabe](https://github.com/boostorg/openmethod/issues/21). -//! //! @tparam Concept The Concept containing this concept. //! @tparam Registry A @ref registry. //! @tparam T A placeholder; leave it to its default, `_self`. diff --git a/include/boost/openmethod/interop/std_any.hpp b/include/boost/openmethod/interop/std_any.hpp index cf47294a..83ac88a4 100644 --- a/include/boost/openmethod/interop/std_any.hpp +++ b/include/boost/openmethod/interop/std_any.hpp @@ -10,6 +10,11 @@ #include #include +// Dispatch on the type contained in a `std::any`. +// +// This interop is based on a design contributed by Steven Watanabe: +// https://github.com/boostorg/openmethod/issues/21 + namespace boost::openmethod { namespace detail { From 235344f28c690b8f9dec63fc6876ef3c8de4360f Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 15 Aug 2026 10:56:19 -0400 Subject: [PATCH 52/64] doc: touch up `any` doc --- doc/modules/ROOT/pages/interop_any.adoc | 28 +++++++------------------ 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/doc/modules/ROOT/pages/interop_any.adoc b/doc/modules/ROOT/pages/interop_any.adoc index d11797f8..82dc2d4f 100644 --- a/doc/modules/ROOT/pages/interop_any.adoc +++ b/doc/modules/ROOT/pages/interop_any.adoc @@ -2,18 +2,9 @@ [#interop_any] ## Interoperation with `any` -A value held in an `any` has a type that is not visible in the static type of -the variable holding the `any`. This section covers the constructs that let a -method look through the wrapper and dispatch on what is really inside. - -### `any` - -An `any` holds a value of almost any type, and remembers which type that is. -That is precisely what a method needs in order to pick an overrider. OpenMethod -can thus dispatch on the type _contained_ in an `any`, in effect treating a set -of otherwise unrelated types as a hierarchy rooted at `std::any`. The types need -not be polymorphic, and need not be related to one another - which makes this a -way of adding behavior to types we do not own, including built-in types. +OpenMethod can take `any` (both the `std` and `boost` flavors) as virtual +arguments, and dispatch on the type of the contained value. For this purpose, it +regards the types as "deriving" from `any`. Support is provided by ``. It is not included by ``, so it must be included explicitly. @@ -28,10 +19,9 @@ xref:error_handling.adoc[Error Handling]. The `any` is then passed like any other virtual argument that is not a `virtual_ptr`: wrapped in `virtual_`, as described in xref:virtual_ptr_alt.adoc[Alternatives to virtual_ptr]. Overriders receive the -_contained_ value, by a reference of a compatible category. An overrider may -also take the `any` itself; since every registered type derives from it, such an -overrider is a catch-all, applying to any contained type that has no more -specific overrider: +_contained_ value by reference. An overrider may also take the `any` itself; +such an overrider is a catch-all, applying to any contained type that has no +more specific overrider: [source,c++] ---- @@ -40,10 +30,8 @@ include::example$virtual_any.cpp[tag=content] #### Mixing with ordinary virtual parameters -An `any` virtual parameter is an ordinary virtual parameter that happens to -resolve through the contained type, so it composes with the others without -restriction. A multi-method can dispatch on an `any` and on a `virtual_ptr`, or -a plain reference, in the same call: +A multi-method can take any combination of ordinary virtual parameters and +virtual `any` in the same call: ```c++ BOOST_OPENMETHOD( From 8f2384b7d665bdfd30a103d42592390590d459c9 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 15 Aug 2026 11:29:05 -0400 Subject: [PATCH 53/64] doc: touch up TypeErasure doc Use Boost.TypeErasure's own vocabulary. "flavor" appears nowhere in its docs or headers; the noun for `any` is "any reference", and what distinguishes it from `any` is the *placeholder*. So: "reference flavors" and "reference-wrapper" become "any references", "the rvalue-reference flavor" becomes "the rvalue-reference placeholder", and "whatever the flavor of the parameter" becomes "whatever the placeholder". "owning any" stays - "owning" is ours, but the distinction is real and the term reads clearly. Keep "wrapper" where it means virtual_any, which is one of ours, and where it means the const-qualified any rather than a reference. Co-Authored-By: Claude Opus 5 --- doc/modules/ROOT/examples/type_erasure.cpp | 2 +- .../ROOT/examples/type_erasure_ref.cpp | 2 +- doc/modules/ROOT/pages/interop_any.adoc | 3 +- .../ROOT/pages/interop_type_erasure.adoc | 68 ++++++--------- doc/modules/ROOT/snippets/type_erasure.cpp | 2 +- .../openmethod/interop/boost_type_erasure.hpp | 82 +++++++++---------- .../boost/openmethod/interop/virtual_any.hpp | 4 +- test/compile_fail_type_erasure_by_value.cpp | 7 +- test/test_dispatch_type_erasure.cpp | 20 ++--- test/test_virtual_any_type_erasure.cpp | 2 +- 10 files changed, 88 insertions(+), 104 deletions(-) diff --git a/doc/modules/ROOT/examples/type_erasure.cpp b/doc/modules/ROOT/examples/type_erasure.cpp index 21920eb4..056db24e 100644 --- a/doc/modules/ROOT/examples/type_erasure.cpp +++ b/doc/modules/ROOT/examples/type_erasure.cpp @@ -28,7 +28,7 @@ struct Dog { std::string name; }; -// The owning flavor, `any`, becomes the common base of the types +// The owning `any`, `any`, becomes the common base of the types // the `any` may bind. BOOST_OPENMETHOD_REGISTER(use_type_erasure_types); diff --git a/doc/modules/ROOT/examples/type_erasure_ref.cpp b/doc/modules/ROOT/examples/type_erasure_ref.cpp index 13daf9f2..a58505f3 100644 --- a/doc/modules/ROOT/examples/type_erasure_ref.cpp +++ b/doc/modules/ROOT/examples/type_erasure_ref.cpp @@ -28,7 +28,7 @@ struct Dog { BOOST_OPENMETHOD_REGISTER(use_type_erasure_types); -// The reference-wrapper flavor is a cheap handle; it is passed by value. +// An any reference is a cheap handle; it is passed by value. BOOST_OPENMETHOD(poke, (virtual_), std::string); BOOST_OPENMETHOD_OVERRIDE(poke, (Dog& dog), std::string) { diff --git a/doc/modules/ROOT/pages/interop_any.adoc b/doc/modules/ROOT/pages/interop_any.adoc index 6acbe7c1..fa39cba3 100644 --- a/doc/modules/ROOT/pages/interop_any.adoc +++ b/doc/modules/ROOT/pages/interop_any.adoc @@ -118,8 +118,7 @@ counterpart: it _borrows_ an `any` that lives elsewhere, bundling its address with the v-table pointer for the value inside it - acquired once, when the handle is created, or taken at no cost from a `virtual_any`. It is a cheap, two-word handle with pointer semantics. Unlike the owning wrapper, it is passed -to methods _by value_, like the reference-wrapper flavors of Boost.TypeErasure's -`any`: +to methods _by value_, like Boost.TypeErasure's any references: ```c++ BOOST_OPENMETHOD(poke, (virtual_any_ref), std::string); diff --git a/doc/modules/ROOT/pages/interop_type_erasure.adoc b/doc/modules/ROOT/pages/interop_type_erasure.adoc index 35447c1f..08c1cc13 100644 --- a/doc/modules/ROOT/pages/interop_type_erasure.adoc +++ b/doc/modules/ROOT/pages/interop_type_erasure.adoc @@ -2,32 +2,25 @@ [#interop_type_erasure] ## Interoperation with Boost.TypeErasure -link:https://www.boost.org/doc/libs/release/doc/html/boost_typeerasure.html[Boost.TypeErasure]'s -`any` erases the type of its content, like `std::any`, but couples that with a -Concept: a compile-time list of the operations the content must support. -OpenMethod can dispatch on the type bound to such an `any`, in the same manner -as for a plain `any` - see xref:interop_any.adoc[Interoperation with `any`]. +Methods can take +link:https://www.boost.org/doc/libs/release/doc/html/boost_typeerasure.html[Boost.TypeErasure] +``any``s as virtual parameters, in the same manner as for a plain ``any``s - see +xref:interop_any.adoc[Interoperation with `any`]. Support is provided by ``. It is not included by ``, so it must be included explicitly. -### `type_erasure::any` - Dispatch resolves on the type returned by `boost::type_erasure::typeid_of`, so -the only requirement placed on the Concept is that it contain -`boost::type_erasure::typeid_<>` - which `relaxed` already implies. +the ``any``'s concept must include `boost::type_erasure::typeid_<>`. -The types the `any` may bind have to be registered. -cpp:use_type_erasure_types[] does that, registering the owning flavor - -`any` - as a class, and each of the types as a class derived from it. -Each Concept gets a root of its own, so ``any``s with different Concepts - and -the plain ``any``s - can coexist in the same registry. A type that is not -registered cannot be dispatched on; a call with such a value bound to the -`any` is a cpp:missing_class[] error - see -xref:error_handling.adoc[Error Handling]. +The types an `any` may bind to must be registered via +cpp:use_type_erasure_types[]. ``any``s with different concepts must each be +registered with the types it may contain. A type that is not registered cannot +be dispatched on; a call with such a value bound to the ``any`` is a +cpp:missing_class[] error - see xref:error_handling.adoc[Error Handling]. -The `any` is passed wrapped in `virtual_`, and overriders receive the _bound_ -value - or, for a catch-all overrider, the `any` itself: +The `any` is wrapped in `virtual_` in the method parameters; overriders receive +the _bound_ value - or, for a catch-all overrider, the `any` itself: [source,c++] ---- @@ -36,7 +29,7 @@ include::example$type_erasure.cpp[tag=content] #### Reference categories -The owning flavor is passed by reference, in any of the three categories - +The `any` is passed by reference, in any of the three categories - passing it by value would copy the bound value on every call, and is rejected at compile time. The category determines what the overriders may take: @@ -78,23 +71,22 @@ case, moving the value out of the `any` is performed by the interop code itself: an overrider taking `Dog&&` receives the bound value ready to be moved from, and the `any` still owns the moved-from object afterwards. -#### The reference-wrapper flavors +#### `any` references -Boost.TypeErasure also has non-owning flavors, `any` and +Boost.TypeErasure also has non-owning ``any``'s, `any` and `any`, which hold a _reference_ to a value stored -elsewhere. They are cheap, two-word handles, and, unlike the owning flavor, -they are passed by value - the idiomatic way to use them as parameters. -Modifications made through a mutable-reference wrapper reach the referent: +elsewhere. They are cheap, two-word handles, and, unlike an owning `any`, they +are passed by value - the idiomatic way to use them as parameters. Modifications +made through a mutable any reference reach the referent: [source,c++] ---- include::example$type_erasure_ref.cpp[tag=content] ---- -Dispatch is on the type _bound at construction_ of the wrapper - never on the -C++ RTTI dynamic type of the referent. The rvalue-reference flavor -(`any`), and placeholders other than `_self`, are not -supported. +Dispatch is on the type _bound at construction_ of the any reference - never on +the C++ RTTI dynamic type of the referent. The rvalue-reference placeholder +(`_self&&`), and placeholders other than `_self`, are not supported. For the same reason as for `std::any`, cpp:final_virtual_ptr[] is _deleted_ for `type_erasure::any`: it would silently produce the v-table of the root @@ -107,8 +99,8 @@ Every call above looks the v-table up in a hash table, keyed on the type the v-table pointer for the value bound to it, acquiring it once, on construction, and maintaining it across assignment and `emplace`. It is to an `any` what cpp:virtual_ptr[] is to a pointer, except that it _owns_ the object: the `any` -is held by value. The Concept needs `relaxed` - for the default constructor -and assignment - and `copy_constructible<>` for copies. +is held by value. The ``any``'s concept must include `relaxed` - for the default +constructor and assignment - and `copy_constructible<>` for copies. [source,c++] ---- @@ -130,7 +122,7 @@ xref:interop_any.adoc[Interoperation with `any`] for the details. cpp:openmethod_vptr[] is an alternative to `virtual_any`, pursuing the same goal - constant-time access to the v-table pointer - from the other side. Instead of wrapping the `any` from the outside, it puts the v-table pointer -inside the dispatch table of the `any` itself, making every flavor of it +inside the dispatch table of the `any` itself, making every `any` on it intrinsically polymorphic, and leaving the call sites unchanged. The two are mutually exclusive: an `any` that carries the concept cannot be wrapped in a `virtual_any` - and does not need to be; wrapping one is rejected at compile @@ -138,17 +130,11 @@ time. `virtual_any` is to an `any` what cpp:virtual_ptr[] is to a pointer, while `openmethod_vptr` is to an `any` what cpp:inplace_vptr_base[] is to a class - see xref:virtual_ptr_alt.adoc[Alternatives to virtual_ptr]. -The `any` already carries a dispatch table of Concept operations, so the -v-table pointer can be one of them. Include it in the Concept, and every -flavor of the `any` gains an operation that returns the registry's -static v-table pointer (`registry::static_vptr`) for the bound type - -instantiated once per bound type, retrieved in constant time. The concept -surfaces the operation as a `boost_openmethod_vptr` overload, which dispatch -prefers over the `vptr` policy, so calls through such an `any` never hash the -result of `typeid_of`. +`openmethod_vptr` is implemented via the cpp:boost_openmethod_vptr[] ADL +customization point. Binding a value to such an `any` also _registers_ its type, as a class derived -from the owning flavor - the registrar is instantiated along with the +from the owning `any` - the registrar is instantiated along with the operation. `use_type_erasure_types` becomes unnecessary for these ``any``s, although the two registration styles may coexist. diff --git a/doc/modules/ROOT/snippets/type_erasure.cpp b/doc/modules/ROOT/snippets/type_erasure.cpp index 872247c7..6455ef3a 100644 --- a/doc/modules/ROOT/snippets/type_erasure.cpp +++ b/doc/modules/ROOT/snippets/type_erasure.cpp @@ -32,7 +32,7 @@ struct Dog { std::string name; }; -// The owning flavor, `any`, becomes the common base of the types +// The owning `any`, `any`, becomes the common base of the types // the `any` may bind. BOOST_OPENMETHOD_REGISTER( use_type_erasure_types); diff --git a/include/boost/openmethod/interop/boost_type_erasure.hpp b/include/boost/openmethod/interop/boost_type_erasure.hpp index 71ab1368..7bfaece7 100644 --- a/include/boost/openmethod/interop/boost_type_erasure.hpp +++ b/include/boost/openmethod/interop/boost_type_erasure.hpp @@ -26,28 +26,28 @@ // The Concept must contain boost::type_erasure::typeid_<> - which // `relaxed` already implies - so that `typeid_of` can identify the // contained value. Dispatch is on the `std::type_info` object returned by -// `typeid_of`: the type of the contained value for the owning flavor -// (`any`), or the type *bound at construction* for the reference -// flavors (`any`, `any`) - never +// `typeid_of`: the type of the contained value for the owning any +// (`any`), or the type *bound at construction* for the any +// references (`any`, `any`) - never // the C++ RTTI dynamic type of the referent. // // Supported virtual parameter forms: // - `virtual_&>`, `virtual_&>`, -// `virtual_&&>` - the owning flavor, by reference, like +// `virtual_&&>` - the owning any, by reference, like // `std::any`; // - `virtual_>` and -// `virtual_>` - the reference-wrapper -// flavors, by value (they are cheap, two-word handles); +// `virtual_>` - the any references, by +// value (they are cheap, two-word handles); // - `virtual_any>` - looks the v-table pointer up once, at // construction. The Concept needs `relaxed` for virtual_any's default // constructor and assignment, and `copy_constructible<>` for copies. // -// The rvalue-reference flavor (`any`), and placeholders -// other than `_self`, are not supported. +// The rvalue-reference placeholder (`_self&&`), and placeholders other +// than `_self`, are not supported. // // In addition, `openmethod_vptr` is a Boost.TypeErasure concept that // stores the v-table pointer for the bound type in the any's own dispatch -// table, making every flavor of the any intrinsically polymorphic: calls +// table, making every any on that Concept intrinsically polymorphic: calls // resolve in constant time, without hashing the result of `typeid_of`, // and binding a value to the any registers its type. // @@ -83,9 +83,9 @@ template constexpr bool te_pass_through = std::is_same_v>, Any>; -// The canonical root class for a Concept: the owning flavor. All the +// The canonical root class for a Concept: the owning any. All the // virtual_traits below use it as their virtual_type, whatever the -// flavor of the parameter, so methods, overriders and +// placeholder of the parameter, so methods, overriders and // use_type_erasure_types agree on a single registered root per Concept. template using type_erasure_root = boost::type_erasure::any< @@ -132,8 +132,8 @@ struct validate_method_parameter< //! obtained via `boost::type_erasure::typeid_of`. `Concept` must contain //! `boost::type_erasure::typeid_<>`; `relaxed` implies it. //! -//! This specialization serves the owning flavor (`any`) and, -//! through a const wrapper, the reference flavors. +//! This specialization serves the owning `any` (`any`) and, +//! through a const `any`, the any references. //! //! @tparam C The `any`'s Concept. //! @tparam T The `any`'s placeholder. @@ -142,7 +142,7 @@ struct validate_method_parameter< //! @see [Interoperation with Boost.TypeErasure](xref:ROOT:interop_type_erasure.adoc) template struct virtual_traits&, Registry> { - //! The type used for dispatch: the owning flavor for `C`. + //! The type used for dispatch: the owning `any` for `C`. using virtual_type = boost::type_erasure::any; //! Returns a const reference to the `any` argument. @@ -169,8 +169,8 @@ struct virtual_traits&, Registry> { //! //! Extracts the bound value using `boost::type_erasure::any_cast`. //! Since the `any` is const, `U` can be a mutable reference only for - //! the mutable-reference flavor (`any`), whose - //! referent stays mutable through a const wrapper. Rvalue references + //! the mutable any reference (`any`), whose + //! referent stays mutable through a const `any`. Rvalue references //! are never allowed; the overloads are removed from the overload //! set. //! @@ -207,7 +207,7 @@ struct virtual_traits&, Registry> { //! @see [Interoperation with Boost.TypeErasure](xref:ROOT:interop_type_erasure.adoc) template struct virtual_traits&, Registry> { - //! The type used for dispatch: the owning flavor for `C`. + //! The type used for dispatch: the owning `any` for `C`. using virtual_type = boost::type_erasure::any; //! Returns a const reference to the `any` argument. @@ -234,7 +234,7 @@ struct virtual_traits&, Registry> { //! //! Extracts the bound value using `boost::type_erasure::any_cast`. //! Supports mutable references (e.g. `Dog&`), except through the - //! const-reference flavor (`any`). `U` cannot + //! const any reference (`any`). `U` cannot //! be an rvalue reference: moving the value out must go through an //! explicit rvalue-reference parameter. The disallowed overloads are //! removed from the overload set. @@ -272,7 +272,7 @@ struct virtual_traits&, Registry> { //! @see [Interoperation with Boost.TypeErasure](xref:ROOT:interop_type_erasure.adoc) template struct virtual_traits&&, Registry> { - //! The type used for dispatch: the owning flavor for `C`. + //! The type used for dispatch: the owning `any` for `C`. using virtual_type = boost::type_erasure::any; //! Returns a const reference to the `any` argument. @@ -300,9 +300,9 @@ struct virtual_traits&&, Registry> { //! Extracts the bound value using `boost::type_erasure::any_cast`. //! `boost::type_erasure::any_cast` has no rvalue overload, so, for an //! rvalue-reference `U`, the result of a mutable-reference cast is - //! moved - only for the owning flavor, since the rvalue-ness of a - //! reference wrapper says nothing about the referent. Casting to a - //! value also moves for the owning flavor, and copies otherwise. The + //! moved - only for the owning `any`, since the rvalue-ness of a + //! any reference says nothing about the referent. Casting to a + //! value also moves for the owning `any`, and copies otherwise. The //! disallowed overloads are removed from the overload set. //! //! @tparam U The target type (e.g. `Dog&&`, `const Dog&`, `Dog`). @@ -330,10 +330,10 @@ struct virtual_traits&&, Registry> { } }; -//! Specialize virtual_traits for the mutable reference-wrapper flavor, +//! Specialize virtual_traits for the mutable any reference, //! `boost::type_erasure::any`, passed by value. //! -//! The reference flavors are cheap, two-word handles; passing them by +//! The any references are cheap, two-word handles; passing them by //! value is the idiomatic way to use them as parameters. Dispatch is on //! the type *bound at construction*, obtained via //! `boost::type_erasure::typeid_of` - not the C++ RTTI dynamic type of @@ -347,7 +347,7 @@ struct virtual_traits&&, Registry> { //! @see [Interoperation with Boost.TypeErasure](xref:ROOT:interop_type_erasure.adoc) template struct virtual_traits, Registry> { - //! The type used for dispatch: the owning flavor for `C`. + //! The type used for dispatch: the owning `any` for `C`. using virtual_type = boost::type_erasure::any; //! Returns a const reference to the `any` argument. @@ -379,7 +379,7 @@ struct virtual_traits, Registry> { //! overloads are removed from the overload set. //! //! @tparam U The target type (e.g. `Dog&`, `const Dog&`, `Dog`). - //! @param arg The reference-wrapper `any` method argument. + //! @param arg The any reference method argument. //! @return The value bound to `arg`, cast to `U`. template< typename U, typename = std::enable_if_t>> @@ -395,10 +395,10 @@ struct virtual_traits, Registry> { } }; -//! Specialize virtual_traits for the const reference-wrapper flavor, +//! Specialize virtual_traits for the const any reference, //! `boost::type_erasure::any`, passed by value. //! -//! The reference flavors are cheap, two-word handles; passing them by +//! The any references are cheap, two-word handles; passing them by //! value is the idiomatic way to use them as parameters. Dispatch is on //! the type *bound at construction*, obtained via //! `boost::type_erasure::typeid_of` - not the C++ RTTI dynamic type of @@ -413,7 +413,7 @@ struct virtual_traits, Registry> { //! @see [Interoperation with Boost.TypeErasure](xref:ROOT:interop_type_erasure.adoc) template struct virtual_traits, Registry> { - //! The type used for dispatch: the owning flavor for `C`. + //! The type used for dispatch: the owning `any` for `C`. using virtual_type = boost::type_erasure::any; //! Returns a const reference to the `any` argument. @@ -443,7 +443,7 @@ struct virtual_traits, Registry> { //! the other overloads are removed from the overload set. //! //! @tparam U The target type (e.g. `const Dog&`, `Dog`). - //! @param arg The reference-wrapper `any` method argument. + //! @param arg The any reference method argument. //! @return The value bound to `arg`, cast to `U`. template< typename U, @@ -465,14 +465,14 @@ struct virtual_traits, Registry> { //! Register the types that a `boost::type_erasure::any` virtual parameter //! may contain. //! -//! Registers the owning flavor of `Any` (i.e. +//! Registers the owning `any` of `Any` (i.e. //! `any::type>`) as a class, and each `T` as a class //! derived from it. This makes the bound types visible to the dispatch //! machinery, which resolves a call on the `type_id` returned by //! `boost::type_erasure::typeid_of`. `Any` may be spelled with any -//! flavor; the root is normalized to the owning flavor, which is also -//! what the virtual_traits use, whatever the flavor of the method -//! parameter. +//! placeholder; the root is normalized to the owning `any`, which is +//! also what the virtual_traits use, whatever the placeholder of the +//! method parameter. //! //! @tparam Any A `boost::type_erasure::any` type. //! @tparam T... The types that may be bound to the `any`, optionally @@ -491,7 +491,7 @@ struct use_type_erasure_types namespace detail { -// Registers Class as deriving from the owning flavor for Concept - the +// Registers Class as deriving from the owning any for Concept - the // same shape use_type_erasure_types produces - when odr-used from // openmethod_vptr::apply. template @@ -504,7 +504,7 @@ use_class_aux>> //! polymorphic. //! //! Including `openmethod_vptr` in a Concept adds an operation, -//! to the dispatch table of every flavor of `any`, that returns +//! to the dispatch table of every `any` on `Concept`, that returns //! the @ref registry::static_vptr for the bound type; and it surfaces the //! operation as a @ref boost_openmethod_vptr overload, which dispatch //! prefers over the registry's `vptr` policy. Calls thus resolve in @@ -512,7 +512,7 @@ use_class_aux>> //! `boost::type_erasure::typeid_of`. //! //! In addition, binding a value to such an `any` registers its type as a -//! class derived from the owning flavor - the same shape +//! class derived from the owning `any` - the same shape //! @ref use_type_erasure_types produces, with which it can coexist. No //! explicit registration is needed for the types bound to an `any` that //! carries this concept. @@ -544,7 +544,7 @@ template< struct openmethod_vptr { //! Returns the v-table pointer for the bound type. //! - //! Also registers `T`, and the owning flavor for `Concept` as its + //! Also registers `T`, and the owning `any` for `Concept` as its //! base, by odr-using their registrars. //! //! @return The @ref registry::static_vptr for `T`. @@ -561,13 +561,13 @@ struct openmethod_vptr { namespace boost::type_erasure { // Surface the openmethod_vptr operation as the boost_openmethod_vptr -// intrinsic hook, injected into the interface of every flavor of any -// whose Concept contains the concept. +// intrinsic hook, injected into the interface of every any whose +// Concept contains the concept. template struct concept_interface< boost::openmethod::openmethod_vptr, Base, T> : Base { // The parameter is a deduced `Self`, constrained to the exact any - // flavor, because MSVC's `/std:c++17` does not imply `/permissive-`: + // type, because MSVC's `/std:c++17` does not imply `/permissive-`: // it injects hidden friends into the enclosing namespace, where // ordinary lookup finds them. A `const derived::type&` // parameter would then make this a candidate for an any over an diff --git a/include/boost/openmethod/interop/virtual_any.hpp b/include/boost/openmethod/interop/virtual_any.hpp index 6c66a75e..0bdf3e15 100644 --- a/include/boost/openmethod/interop/virtual_any.hpp +++ b/include/boost/openmethod/interop/virtual_any.hpp @@ -518,8 +518,8 @@ struct select_overrider_virtual_type_aux< //! the v-table pointer for the contained value, so methods dispatch on //! the contained type without looking it up on every call. It is a //! cheap, two-word handle with pointer semantics - copying it copies the -//! two words - and, like the reference-wrapper flavors of -//! Boost.TypeErasure's `any`, it is passed to methods *by value*. +//! two words - and, like Boost.TypeErasure's any references, it is +//! passed to methods *by value*. //! //! `Any` may be const-qualified: through `virtual_any_ref`, //! overriders can only take the contained value by value or by const diff --git a/test/compile_fail_type_erasure_by_value.cpp b/test/compile_fail_type_erasure_by_value.cpp index c1c498c9..ff526250 100644 --- a/test/compile_fail_type_erasure_by_value.cpp +++ b/test/compile_fail_type_erasure_by_value.cpp @@ -23,10 +23,9 @@ struct Dog { BOOST_OPENMETHOD_REGISTER(use_type_erasure_types); -// The owning flavor must be passed by reference: by value, it would copy -// the `any` - and its payload - on every call. (The reference-wrapper -// flavors, any and any, may be passed by -// value.) +// The owning any must be passed by reference: by value, it would copy +// the `any` - and its payload - on every call. (The any references, +// any and any, may be passed by value.) BOOST_OPENMETHOD(name, (virtual_), std::string); int main() { diff --git a/test/test_dispatch_type_erasure.cpp b/test/test_dispatch_type_erasure.cpp index 1dcfd25b..75456a8a 100644 --- a/test/test_dispatch_type_erasure.cpp +++ b/test/test_dispatch_type_erasure.cpp @@ -160,8 +160,8 @@ BOOST_AUTO_TEST_CASE(type_erasure_by_xvalue_ref) { namespace BOOST_OPENMETHOD_GENSYM { // ----------------------------------------------------------------------------- -// pass virtual args as any - the mutable reference-wrapper -// flavor - by value +// pass virtual args as any - the mutable any reference - by +// value MAKE_CLASSES(); @@ -177,10 +177,10 @@ BOOST_OPENMETHOD_OVERRIDE(poke, (int& value), std::string) { return "poked"; } -BOOST_AUTO_TEST_CASE(type_erasure_ref_wrapper_by_value) { +BOOST_AUTO_TEST_CASE(type_erasure_any_ref_by_value) { initialize(trace()); - // the wrapper is a cheap handle; mutations reach the referents + // the any reference is a cheap handle; mutations reach the referents Dog snoopy{"Snoopy"}; int count = 41; @@ -195,8 +195,8 @@ BOOST_AUTO_TEST_CASE(type_erasure_ref_wrapper_by_value) { namespace BOOST_OPENMETHOD_GENSYM { // ----------------------------------------------------------------------------- -// pass virtual args as any - the const reference-wrapper -// flavor - by value +// pass virtual args as any - the const any reference - +// by value MAKE_CLASSES(); @@ -206,12 +206,12 @@ BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { return dog.name + " the dog"; } -// the catch-all receives a copy of the wrapper - still a cheap handle +// the catch-all receives a copy of the any reference - still a cheap handle BOOST_OPENMETHOD_OVERRIDE(name, (erased_cref arg), std::string) { return te::is_empty(arg) ? "nothing" : "something"; } -BOOST_AUTO_TEST_CASE(type_erasure_cref_wrapper_by_value) { +BOOST_AUTO_TEST_CASE(type_erasure_const_any_ref_by_value) { initialize(trace()); Dog snoopy{"Snoopy"}; @@ -270,7 +270,7 @@ struct Dispatchable : boost::mpl::vector< using dispatchable = te::any; using dispatchable_ref = te::any; -// the intrinsic hook is found for every flavor, so dispatch prefers it +// the intrinsic hook is found for every any, so dispatch prefers it // over the vptr policy's hash lookup static_assert(detail::has_vptr_fn); static_assert(detail::has_vptr_fn); @@ -311,7 +311,7 @@ BOOST_AUTO_TEST_CASE(type_erasure_openmethod_vptr_concept) { const dispatchable felix(std::string{"Felix"}); BOOST_TEST(name(felix) == "something"); - // the reference-wrapper flavor takes the fast path too + // the any reference takes the fast path too Dog snoopy{"Snoopy"}; BOOST_TEST(poke(dispatchable_ref(snoopy)) == "Snoopy!"); BOOST_TEST(snoopy.name == "Snoopy!"); diff --git a/test/test_virtual_any_type_erasure.cpp b/test/test_virtual_any_type_erasure.cpp index 4a0b004a..f5133e21 100644 --- a/test/test_virtual_any_type_erasure.cpp +++ b/test/test_virtual_any_type_erasure.cpp @@ -85,7 +85,7 @@ BOOST_AUTO_TEST_CASE(virtual_any_by_const_ref) { BOOST_TEST(name(Dog{"Fido"}) == "Fido the dog"); // `int` is registered, but has no specific overrider: the catch-all, - // registered for the owning flavor, applies + // registered for the owning any, applies BOOST_TEST(name(42) == "something"); } } // namespace BOOST_OPENMETHOD_GENSYM From 1f8ed775c3cacd2b7a33cea52d1c4ef29ea84372 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 15 Aug 2026 16:44:00 -0400 Subject: [PATCH 54/64] doc: add the virtual_traits::vptr step to the dispatch lists method::vptr tries virtual_traits::vptr between the boost_openmethod_vptr hook and the vptr policy - it is what the any interop rides on - but the "how a vptr is deduced" lists on `method` and BOOST_OPENMETHOD only had three steps. Co-Authored-By: Claude Opus 5 --- include/boost/openmethod/core.hpp | 4 +++- include/boost/openmethod/macros.hpp | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index 6cf49e2f..791c1d54 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -1980,7 +1980,9 @@ struct validate_method_parameter< //! 2. If @ref boost_openmethod_vptr can be called with `result` and a //! `Registry*`, and it returns a `vptr_type`, call it. //! -//! 3. Call the @ref policies::VptrFn::dynamic_vptr of the registry's `vptr` +//! 3. If @ref virtual_traits provides a `vptr` function, call it. +//! +//! 4. Call the @ref policies::VptrFn::dynamic_vptr of the registry's `vptr` //! policy. //! //! @par N2216 Handling of Ambiguous Calls diff --git a/include/boost/openmethod/macros.hpp b/include/boost/openmethod/macros.hpp index 9fcbc84c..87371943 100644 --- a/include/boost/openmethod/macros.hpp +++ b/include/boost/openmethod/macros.hpp @@ -163,6 +163,8 @@ inline constexpr bool method_not_found = false; //! can be called with `result` and a `Registry*`, and it returns a //! `vptr_type`, call it. //! +//! @li If `virtual_traits` provides a `vptr` function, call it. +//! //! @li Call the //! [dynamic_vptr](xref:reference:boost/openmethod/policies/VptrFn/dynamic_vptr.adoc) //! of the registry's `vptr` policy. From 161fdf8c0c7391cbe94e3deea1177b32c13476f3 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 15 Aug 2026 16:47:23 -0400 Subject: [PATCH 55/64] doc: fix an article Co-Authored-By: Claude Opus 5 --- include/boost/openmethod/interop/boost_type_erasure.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/boost/openmethod/interop/boost_type_erasure.hpp b/include/boost/openmethod/interop/boost_type_erasure.hpp index 7bfaece7..16db842b 100644 --- a/include/boost/openmethod/interop/boost_type_erasure.hpp +++ b/include/boost/openmethod/interop/boost_type_erasure.hpp @@ -300,7 +300,7 @@ struct virtual_traits&&, Registry> { //! Extracts the bound value using `boost::type_erasure::any_cast`. //! `boost::type_erasure::any_cast` has no rvalue overload, so, for an //! rvalue-reference `U`, the result of a mutable-reference cast is - //! moved - only for the owning `any`, since the rvalue-ness of a + //! moved - only for the owning `any`, since the rvalue-ness of an //! any reference says nothing about the referent. Casting to a //! value also moves for the owning `any`, and copies otherwise. The //! disallowed overloads are removed from the overload set. From a01efedc2b0e29e6b86158027c1aad92204b7048 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 16 Aug 2026 11:11:16 -0400 Subject: [PATCH 56/64] doc: more `any` touch-up --- .../examples/virtual_ptr_alt/1/virtual_ptr_alt.cpp | 1 - doc/modules/ROOT/pages/interop_any.adoc | 13 ++++++++----- doc/modules/ROOT/pages/ref_headers.adoc | 2 +- doc/modules/ROOT/snippets/virtual_any.cpp | 2 +- include/boost/openmethod/interop/virtual_any.hpp | 10 +++++----- test/test_virtual_any_ref.cpp | 4 ++-- 6 files changed, 17 insertions(+), 15 deletions(-) diff --git a/doc/modules/ROOT/examples/virtual_ptr_alt/1/virtual_ptr_alt.cpp b/doc/modules/ROOT/examples/virtual_ptr_alt/1/virtual_ptr_alt.cpp index 56bba033..460d9bc1 100644 --- a/doc/modules/ROOT/examples/virtual_ptr_alt/1/virtual_ptr_alt.cpp +++ b/doc/modules/ROOT/examples/virtual_ptr_alt/1/virtual_ptr_alt.cpp @@ -28,7 +28,6 @@ struct Times : Node { const Node& left; const Node& right; }; -// tag::content[] #include #include diff --git a/doc/modules/ROOT/pages/interop_any.adoc b/doc/modules/ROOT/pages/interop_any.adoc index 82dc2d4f..8802ac27 100644 --- a/doc/modules/ROOT/pages/interop_any.adoc +++ b/doc/modules/ROOT/pages/interop_any.adoc @@ -6,6 +6,8 @@ OpenMethod can take `any` (both the `std` and `boost` flavors) as virtual arguments, and dispatch on the type of the contained value. For this purpose, it regards the types as "deriving" from `any`. +#### `std::any` + Support is provided by ``. It is not included by ``, so it must be included explicitly. @@ -89,8 +91,8 @@ Every call above looks the v-table up in a hash table, keyed on the type the `any` contains. cpp:virtual_std_any[] - an alias for `virtual_any` - removes that cost: it bundles an `any` with the v-table pointer for the value inside it, acquiring it once, on construction, and maintaining it across -assignment and `emplace`. It is to an `any` what cpp:virtual_ptr[] is to a -pointer, except that it _owns_ the object: the `any` is held by value. +assignment and `emplace`. It is similar to cpp:virtual_ptr[], except that it +_owns_ the object: the `any` is held by value. The pointer comes from a lookup when the `virtual_std_any` is built from an existing `any`, and from a static variable - no lookup at all - when it is built @@ -114,7 +116,7 @@ the v-table of the `any` root class rather than the one for the contained value. #### `virtual_any_ref` `virtual_std_any` owns its `any`. cpp:virtual_any_ref[] is its non-owning -counterpart: it _borrows_ an `any` that lives elsewhere, bundling its address +counterpart: it _refers to_ an `any` that lives elsewhere, bundling its address with the v-table pointer for the value inside it - acquired once, when the handle is created, or taken at no cost from a `virtual_any`. It is a cheap, two-word handle with pointer semantics. Unlike the owning wrapper, it is passed @@ -136,8 +138,9 @@ overriders receive the contained value by value or by const reference only. A mutable handle converts to a const one. A plain value does not convert to a `virtual_any_ref` - there is no `any` for -the handle to borrow - so `BOOST_OPENMETHOD_OVERRIDE`, which locates the method -by convertibility, cannot register overriders that take the contained value. +the handle to refer to - so `BOOST_OPENMETHOD_OVERRIDE`, which locates the +method by convertibility, cannot register overriders that take the contained +value. Register them with the core API instead, as in the `virtual_` case above; the catch-all overrider, which takes the handle itself, can use the macro. diff --git a/doc/modules/ROOT/pages/ref_headers.adoc b/doc/modules/ROOT/pages/ref_headers.adoc index 07309b7a..afb5774e 100644 --- a/doc/modules/ROOT/pages/ref_headers.adoc +++ b/doc/modules/ROOT/pages/ref_headers.adoc @@ -83,7 +83,7 @@ Provides a `virtual_traits` specialization that makes it possible to use a Provides `virtual_any`, a wide `any` that combines an `any`, held by value, with a pointer to the v-table for the contained value - similar to `virtual_ptr`. -Also provides `virtual_any_ref`, a non-owning counterpart that borrows an +Also provides `virtual_any_ref`, a non-owning counterpart that refers to an existing `any`. [#std_any] diff --git a/doc/modules/ROOT/snippets/virtual_any.cpp b/doc/modules/ROOT/snippets/virtual_any.cpp index b832239b..63dc95cd 100644 --- a/doc/modules/ROOT/snippets/virtual_any.cpp +++ b/doc/modules/ROOT/snippets/virtual_any.cpp @@ -178,7 +178,7 @@ BOOST_AUTO_TEST_CASE(virtual_any_ref_examples) { // tag::ref_dispatch[] std::any spot_any = Dog{"Spot"}; - // one lookup; the handle borrows the `any` + // one lookup; the handle refers to the `any` virtual_any_ref spot = spot_any; std::cout << poke(spot) << "\n"; // Spot! diff --git a/include/boost/openmethod/interop/virtual_any.hpp b/include/boost/openmethod/interop/virtual_any.hpp index 109e1b00..f34491a6 100644 --- a/include/boost/openmethod/interop/virtual_any.hpp +++ b/include/boost/openmethod/interop/virtual_any.hpp @@ -503,7 +503,7 @@ struct select_overrider_virtual_type_aux< //! a v-table. //! //! `virtual_any_ref` is the non-owning counterpart of @ref virtual_any: -//! it *borrows* an existing `any` instead of holding a copy, and carries +//! it *refers to* an existing `any` instead of holding a copy, and carries //! the v-table pointer for the contained value, so methods dispatch on //! the contained type without looking it up on every call. It is a //! cheap, two-word handle with pointer semantics - copying it copies the @@ -573,21 +573,21 @@ class virtual_any_ref { detail::acquire_vptr(other))) { } - //! A `virtual_any_ref` cannot borrow a temporary `any`. + //! A `virtual_any_ref` cannot refer to a temporary `any`. virtual_any_ref(std::remove_const_t&&) = delete; //! Construct from a `virtual_any`. //! - //! Borrows the `any` held by `other`, and copies its v-table pointer + //! Refers to the `any` held by `other`, and copies its v-table pointer //! - no lookup is involved. A `virtual_any_ref` can - //! borrow from a const `virtual_any`; a mutable one requires a + //! refer to a const `virtual_any`; a mutable one requires a //! mutable `virtual_any`. //! //! @param other A `virtual_any` lvalue. virtual_any_ref(owner_type& other) : obj(&other.obj), vp(other.vp) { } - //! A `virtual_any_ref` cannot borrow a temporary `virtual_any`. + //! A `virtual_any_ref` cannot refer to a temporary `virtual_any`. virtual_any_ref(std::remove_const_t&&) = delete; //! Convert a mutable `virtual_any_ref` to a const one. diff --git a/test/test_virtual_any_ref.cpp b/test/test_virtual_any_ref.cpp index 70967ee2..7904c966 100644 --- a/test/test_virtual_any_ref.cpp +++ b/test/test_virtual_any_ref.cpp @@ -126,7 +126,7 @@ BOOST_OPENMETHOD_REGISTER(bump_method::override); BOOST_AUTO_TEST_CASE(virtual_any_ref_mutable) { initialize(trace()); - // the handle borrows the `any`; mutations reach the referent + // the handle refers to the `any`; mutations reach the referent std::any spot_any(Dog{"Spot"}); BOOST_TEST(bump(spot_any) == "Spot Jr. the dog"); BOOST_TEST(std::any_cast(spot_any).name == "Spot Jr."); @@ -136,7 +136,7 @@ BOOST_AUTO_TEST_CASE(virtual_any_ref_mutable) { BOOST_TEST(bump(answer) == "bumped"); BOOST_TEST(std::any_cast(answer_any) == 42); - // borrowing from a virtual_any: mutations reach the owner's value + // referring to a virtual_any: mutations reach the owner's value virtual_std_any rex = Dog{"Rex"}; virtual_any_ref rex_ref = rex; BOOST_TEST(rex_ref.vptr() == rex.vptr()); From 156b3435537c4ec008d9192851a72f23db2f84c1 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 16 Aug 2026 11:14:51 -0400 Subject: [PATCH 57/64] doc: drop "borrow" from the type_erasure interop prose "Borrowing" is Rust terminology. Say that virtual_any_ref refers to an existing any, and that the any reference's referent is not owned. Also reflow a paragraph in interop_any.adoc left ragged by the earlier rewording. Co-Authored-By: Claude Opus 5 --- doc/modules/ROOT/pages/interop_any.adoc | 5 ++--- doc/modules/ROOT/pages/interop_type_erasure.adoc | 2 +- include/boost/openmethod/interop/boost_type_erasure.hpp | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/doc/modules/ROOT/pages/interop_any.adoc b/doc/modules/ROOT/pages/interop_any.adoc index af33c178..080650a8 100644 --- a/doc/modules/ROOT/pages/interop_any.adoc +++ b/doc/modules/ROOT/pages/interop_any.adoc @@ -139,9 +139,8 @@ mutable handle converts to a const one. A plain value does not convert to a `virtual_any_ref` - there is no `any` for the handle to refer to - so `BOOST_OPENMETHOD_OVERRIDE`, which locates the method by convertibility, cannot register overriders that take the contained -value. -Register them with the core API instead, as in the `virtual_` case -above; the catch-all overrider, which takes the handle itself, can use the +value. Register them with the core API instead, as in the `virtual_` +case above; the catch-all overrider, which takes the handle itself, can use the macro. The handle does not track its referent: if the value inside the `any` is diff --git a/doc/modules/ROOT/pages/interop_type_erasure.adoc b/doc/modules/ROOT/pages/interop_type_erasure.adoc index 08c1cc13..c4d9cf20 100644 --- a/doc/modules/ROOT/pages/interop_type_erasure.adoc +++ b/doc/modules/ROOT/pages/interop_type_erasure.adoc @@ -110,7 +110,7 @@ include::example$type_erasure_virtual_any.cpp[tag=content] The wrapper is passed by reference, in any of the three categories, and the overriders receive the _bound_ value, exactly as for `virtual_&>` - or the wrapper itself, for a catch-all overrider. -cpp:virtual_any_ref[] is the non-owning counterpart: it _borrows_ an existing +cpp:virtual_any_ref[] is the non-owning counterpart: it _refers to_ an existing `any`, looking the v-table pointer up once, or taking it at no cost from a `virtual_any`. Both work with `type_erasure::any` for the same reason they work with `std::any` and `boost::any` - the interop specializes diff --git a/include/boost/openmethod/interop/boost_type_erasure.hpp b/include/boost/openmethod/interop/boost_type_erasure.hpp index 16db842b..7905c8d4 100644 --- a/include/boost/openmethod/interop/boost_type_erasure.hpp +++ b/include/boost/openmethod/interop/boost_type_erasure.hpp @@ -375,8 +375,8 @@ struct virtual_traits, Registry> { //! Extracts the referent using `boost::type_erasure::any_cast`. //! Supports mutable references (e.g. `Dog&`); modifications through //! the result are visible through the referent. `U` cannot be an - //! rvalue reference - the referent is borrowed, not owned; the - //! overloads are removed from the overload set. + //! rvalue reference - the referent is not owned; the overloads are + //! removed from the overload set. //! //! @tparam U The target type (e.g. `Dog&`, `const Dog&`, `Dog`). //! @param arg The any reference method argument. From 49bfbf5924ce11d9a206f54b6c6300e03e73ec43 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 16 Aug 2026 13:10:00 -0400 Subject: [PATCH 58/64] require std_rtti in the any interop Dispatching on a std::any or boost::any keys on the std::type_info returned by any::type(). That is a valid type_id only for a registry whose rtti policy identifies classes by &typeid(T). Under any other policy the key is meaningless, and type_id being const void*, the conversion compiles silently and the call resolves to the wrong v-table or reports a spurious missing_class at run time. Assert the requirement in virtual_traits::vptr, via one detail helper per header, and document it in the reference comments and the guide. The assert is in the vptr body rather than at class scope so that it fires only when the RTTI-based lookup is actually used. Co-Authored-By: Claude Opus 5 --- doc/modules/ROOT/pages/interop_any.adoc | 8 +++ .../boost/openmethod/interop/boost_any.hpp | 27 +++++++- include/boost/openmethod/interop/std_any.hpp | 30 ++++++++- test/CMakeLists.txt | 2 + test/compile_fail_std_any_custom_rtti.cpp | 61 +++++++++++++++++++ 5 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 test/compile_fail_std_any_custom_rtti.cpp diff --git a/doc/modules/ROOT/pages/interop_any.adoc b/doc/modules/ROOT/pages/interop_any.adoc index 8802ac27..a2e4a829 100644 --- a/doc/modules/ROOT/pages/interop_any.adoc +++ b/doc/modules/ROOT/pages/interop_any.adoc @@ -6,6 +6,14 @@ OpenMethod can take `any` (both the `std` and `boost` flavors) as virtual arguments, and dispatch on the type of the contained value. For this purpose, it regards the types as "deriving" from `any`. +#### Requirements + +Dispatch keys on the `std::type_info` returned by `any::type()`, so the +registry's `rtti` policy must be cpp:std_rtti[], or a policy derived from it. +`default_registry` and `indirect_registry` both qualify. A registry with, say, +cpp:static_rtti[] identifies classes by a different kind of `type_id`, and would +look up the wrong v-table; the requirement is enforced with a `static_assert`. + #### `std::any` Support is provided by ``. It is not diff --git a/include/boost/openmethod/interop/boost_any.hpp b/include/boost/openmethod/interop/boost_any.hpp index e8d58473..6456de02 100644 --- a/include/boost/openmethod/interop/boost_any.hpp +++ b/include/boost/openmethod/interop/boost_any.hpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace boost::openmethod { @@ -26,6 +27,20 @@ template struct validate_method_parameter, 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 +// `const void*`, nothing would diagnose it. +template +constexpr void assert_std_rtti_boost_any() { + static_assert( + std::is_base_of_v< + policies::std_rtti, + find_first_derived_of< + policies::rtti, typename Registry::policy_list>>, + "requires standard RTTI"); +} + } // namespace detail //! Specialize virtual_traits for `const boost::any&` (const reference). @@ -52,7 +67,8 @@ struct virtual_traits { //! `boost::any::type()`. This requires the registry's @ref rtti policy to //! identify classes by `&typeid(T)`, as @ref std_rtti does; //! `boost::any::type()` yields the same `std::type_info` object, provided - //! Boost.TypeIndex uses `stl_type_index`. + //! Boost.TypeIndex uses `stl_type_index`. The requirement is enforced with + //! a `static_assert`. //! //! Passes the type id to the registry's @ref policies::vptr policy, which //! must provide @ref policies::VptrFn::vptr. Both @@ -70,6 +86,7 @@ struct virtual_traits { //! @param arg A reference to a const `any`. //! @return A reference to the v-table pointer for the stored value. static auto vptr(const boost::any& arg) -> const vptr_type& { + detail::assert_std_rtti_boost_any(); return Registry::vptr::vptr(&arg.type()); } @@ -127,7 +144,8 @@ struct virtual_traits { //! `boost::any::type()`. This requires the registry's @ref rtti policy to //! identify classes by `&typeid(T)`, as @ref std_rtti does; //! `boost::any::type()` yields the same `std::type_info` object, provided - //! Boost.TypeIndex uses `stl_type_index`. + //! Boost.TypeIndex uses `stl_type_index`. The requirement is enforced with + //! a `static_assert`. //! //! Passes the type id to the registry's @ref policies::vptr policy, which //! must provide @ref policies::VptrFn::vptr. Both @@ -145,6 +163,7 @@ struct virtual_traits { //! @param arg A reference to a `boost::any`. //! @return A reference to the v-table pointer for the stored value. static auto vptr(const boost::any& arg) -> const vptr_type& { + detail::assert_std_rtti_boost_any(); return Registry::vptr::vptr(&arg.type()); } @@ -202,7 +221,8 @@ struct virtual_traits { //! `boost::any::type()`. This requires the registry's @ref rtti policy to //! identify classes by `&typeid(T)`, as @ref std_rtti does; //! `boost::any::type()` yields the same `std::type_info` object, provided - //! Boost.TypeIndex uses `stl_type_index`. + //! Boost.TypeIndex uses `stl_type_index`. The requirement is enforced with + //! a `static_assert`. //! //! Passes the type id to the registry's @ref policies::vptr policy, which //! must provide @ref policies::VptrFn::vptr. Both @@ -220,6 +240,7 @@ struct virtual_traits { //! @param arg A reference to a `boost::any`. //! @return A reference to the v-table pointer for the stored value. static auto vptr(const boost::any& arg) -> const vptr_type& { + detail::assert_std_rtti_boost_any(); return Registry::vptr::vptr(&arg.type()); } diff --git a/include/boost/openmethod/interop/std_any.hpp b/include/boost/openmethod/interop/std_any.hpp index 54772417..a5da087d 100644 --- a/include/boost/openmethod/interop/std_any.hpp +++ b/include/boost/openmethod/interop/std_any.hpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace boost::openmethod { @@ -26,6 +27,20 @@ template struct validate_method_parameter, 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 +// `const void*`, nothing would diagnose it. +template +constexpr void assert_std_rtti_std_any() { + static_assert( + std::is_base_of_v< + policies::std_rtti, + find_first_derived_of< + policies::rtti, typename Registry::policy_list>>, + "requires standard RTTI"); +} + } // namespace detail //! Specialize virtual_traits for `const std::any&` (const reference). @@ -49,7 +64,9 @@ struct virtual_traits { //! Returns a *reference* to a v-table pointer for an object. //! //! Acquires the @ref type_id of the value stored in `arg`, using - //! `std::any::type()`. + //! `std::any::type()`. This requires the registry's @ref rtti policy to + //! derive from @ref std_rtti, which identifies classes by `&typeid(T)`; + //! the requirement is enforced with a `static_assert`. //! //! Passes it to the registry's @ref policies::vptr policy, which must //! provide @ref policies::VptrFn::vptr. Both @ref policies::vptr_vector @@ -67,6 +84,7 @@ struct virtual_traits { //! @param arg A reference to a const `any`. //! @return A reference to the v-table pointer for the stored value. static auto vptr(const std::any& arg) -> const vptr_type& { + detail::assert_std_rtti_std_any(); return Registry::vptr::vptr(&arg.type()); } @@ -113,7 +131,9 @@ struct virtual_traits { //! Returns a *reference* to a v-table pointer for an object. //! //! Acquires the @ref type_id of the value stored in `arg`, using - //! `std::any::type()`. + //! `std::any::type()`. This requires the registry's @ref rtti policy to + //! derive from @ref std_rtti, which identifies classes by `&typeid(T)`; + //! the requirement is enforced with a `static_assert`. //! //! Passes it to the registry's @ref policies::vptr policy, which must //! provide @ref policies::VptrFn::vptr. Both @ref policies::vptr_vector @@ -131,6 +151,7 @@ struct virtual_traits { //! @param arg A reference to a `std::any`. //! @return A reference to the v-table pointer for the stored value. static auto vptr(const std::any& arg) -> const vptr_type& { + detail::assert_std_rtti_std_any(); return Registry::vptr::vptr(&arg.type()); } @@ -178,7 +199,9 @@ struct virtual_traits { //! Returns a *reference* to a v-table pointer for an object. //! //! Acquires the @ref type_id of the value stored in `arg`, using - //! `std::any::type()`. + //! `std::any::type()`. This requires the registry's @ref rtti policy to + //! derive from @ref std_rtti, which identifies classes by `&typeid(T)`; + //! the requirement is enforced with a `static_assert`. //! //! Passes it to the registry's @ref policies::vptr policy, which must //! provide @ref policies::VptrFn::vptr. Both @ref policies::vptr_vector @@ -196,6 +219,7 @@ struct virtual_traits { //! @param arg A reference to a const `any`. //! @return A reference to the v-table pointer for the stored value. static auto vptr(const std::any& arg) -> const vptr_type& { + detail::assert_std_rtti_std_any(); return Registry::vptr::vptr(&arg.type()); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0edda63c..15759641 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -175,6 +175,8 @@ openmethod_compile_fail_test( openmethod_compile_fail_test( compile_fail_virtual_ptr_inplace_vptr "do not wrap an object that has a boost_openmethod_vptr overload") +openmethod_compile_fail_test( + compile_fail_std_any_custom_rtti "requires standard RTTI") if (TARGET Boost::dll) add_subdirectory(dynamic_loading) diff --git a/test/compile_fail_std_any_custom_rtti.cpp b/test/compile_fail_std_any_custom_rtti.cpp new file mode 100644 index 00000000..de4a9e13 --- /dev/null +++ b/test/compile_fail_std_any_custom_rtti.cpp @@ -0,0 +1,61 @@ +// Copyright (c) 2018-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +#include + +#include +#include + +using namespace boost::openmethod; + +template +struct type_tag { + static constexpr char id = 0; +}; + +// A complete rtti policy that identifies classes by the address of a per-class +// static variable, rather than by `&typeid(T)`. Nothing else in the library +// objects to it - only the `any` interop does. +struct custom_rtti : policies::rtti { + template + struct fn : defaults { + template + static constexpr bool is_polymorphic = false; + + template + static auto static_type() -> type_id { + return &type_tag::id; + } + + template + static auto dynamic_type(const T&) -> type_id { + return &type_tag::id; + } + }; +}; + +struct custom_rtti_registry + : default_registry::with::without {}; + +struct Dog { + std::string name; +}; + +BOOST_OPENMETHOD_REGISTER(use_std_any_types); + +// Dispatching on a `std::any` keys on the `std::type_info` returned by +// `std::any::type()`, so the registry's rtti policy must identify classes the +// same way. This one does not: the lookup key would be meaningless, and +// `type_id` being `const void*`, the call would otherwise compile silently. +BOOST_OPENMETHOD( + name, (virtual_), std::string, custom_rtti_registry); + +int main() { + // Call the method: declaring it is not enough to instantiate it on + // every compiler, and the guard lives in `virtual_traits::vptr`. + std::any dog = Dog{"Snoopy"}; + return name(dog).size(); +} From 8027dd98124f3a7459a02aa4b3f28e622fdd255c Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 16 Aug 2026 13:12:23 -0400 Subject: [PATCH 59/64] require std_rtti in the type_erasure interop Dispatching on a type_erasure::any keys on the std::type_info returned by boost::type_erasure::typeid_of, so the registry's rtti policy must identify classes the same way. Assert it in virtual_traits::vptr, as for std::any and boost::any. The openmethod_vptr concept takes the vptr from the any's own dispatch table and never calls typeid_of, so it is deliberately not covered: the assert lives in the vptr body rather than at class scope, and does not fire for that path. Co-Authored-By: Claude Opus 5 --- .../ROOT/pages/interop_type_erasure.adoc | 11 +++ .../openmethod/interop/boost_type_erasure.hpp | 41 +++++++++++ test/CMakeLists.txt | 2 + .../compile_fail_type_erasure_custom_rtti.cpp | 69 +++++++++++++++++++ 4 files changed, 123 insertions(+) create mode 100644 test/compile_fail_type_erasure_custom_rtti.cpp diff --git a/doc/modules/ROOT/pages/interop_type_erasure.adoc b/doc/modules/ROOT/pages/interop_type_erasure.adoc index c4d9cf20..8702bf88 100644 --- a/doc/modules/ROOT/pages/interop_type_erasure.adoc +++ b/doc/modules/ROOT/pages/interop_type_erasure.adoc @@ -10,9 +10,20 @@ xref:interop_any.adoc[Interoperation with `any`]. Support is provided by ``. It is not included by ``, so it must be included explicitly. +#### Requirements + Dispatch resolves on the type returned by `boost::type_erasure::typeid_of`, so the ``any``'s concept must include `boost::type_erasure::typeid_<>`. +`typeid_of` returns a `std::type_info`, so the registry's `rtti` policy must +also identify classes that way: it must be cpp:std_rtti[], or a policy derived +from it. `default_registry` and `indirect_registry` both qualify. A registry +with, say, cpp:static_rtti[] identifies classes by a different kind of +`type_id`, and would look up the wrong v-table; the requirement is enforced with +a `static_assert`. The cpp:openmethod_vptr[] concept takes the v-table pointer +from the ``any``'s own dispatch table and never calls `typeid_of`, so it does +not require `std_rtti`. + The types an `any` may bind to must be registered via cpp:use_type_erasure_types[]. ``any``s with different concepts must each be registered with the types it may contain. A type that is not registered cannot diff --git a/include/boost/openmethod/interop/boost_type_erasure.hpp b/include/boost/openmethod/interop/boost_type_erasure.hpp index 7905c8d4..e8742f6c 100644 --- a/include/boost/openmethod/interop/boost_type_erasure.hpp +++ b/include/boost/openmethod/interop/boost_type_erasure.hpp @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -124,6 +125,21 @@ struct validate_method_parameter< false_t, "an owning type_erasure::any must be passed by reference"); }; +// `boost::type_erasure::typeid_of` 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 +// `const void*`, nothing would diagnose it. The `openmethod_vptr` concept does +// not go through `typeid_of`, and is deliberately not covered. +template +constexpr void assert_std_rtti_type_erasure() { + static_assert( + std::is_base_of_v< + policies::std_rtti, + find_first_derived_of< + policies::rtti, typename Registry::policy_list>>, + "requires standard RTTI"); +} + } // namespace detail //! Specialize virtual_traits for `const boost::type_erasure::any&`. @@ -158,10 +174,15 @@ struct virtual_traits&, Registry> { //! Looks up the @ref type_id returned by //! `boost::type_erasure::typeid_of` in the registry's `vptr` policy. //! + //! This requires the registry's @ref rtti policy to derive from + //! @ref std_rtti, which identifies classes by `&typeid(T)`, as + //! `typeid_of` does; the requirement is enforced with a `static_assert`. + //! //! @param arg A reference to a const `any`. //! @return A reference to the v-table pointer for the bound value. static auto vptr(const boost::type_erasure::any& arg) -> const vptr_type& { + detail::assert_std_rtti_type_erasure(); return Registry::vptr::vptr(&boost::type_erasure::typeid_of(arg)); } @@ -223,10 +244,15 @@ struct virtual_traits&, Registry> { //! Looks up the @ref type_id returned by //! `boost::type_erasure::typeid_of` in the registry's `vptr` policy. //! + //! This requires the registry's @ref rtti policy to derive from + //! @ref std_rtti, which identifies classes by `&typeid(T)`, as + //! `typeid_of` does; the requirement is enforced with a `static_assert`. + //! //! @param arg A reference to an `any`. //! @return A reference to the v-table pointer for the bound value. static auto vptr(const boost::type_erasure::any& arg) -> const vptr_type& { + detail::assert_std_rtti_type_erasure(); return Registry::vptr::vptr(&boost::type_erasure::typeid_of(arg)); } @@ -288,10 +314,15 @@ struct virtual_traits&&, Registry> { //! Looks up the @ref type_id returned by //! `boost::type_erasure::typeid_of` in the registry's `vptr` policy. //! + //! This requires the registry's @ref rtti policy to derive from + //! @ref std_rtti, which identifies classes by `&typeid(T)`, as + //! `typeid_of` does; the requirement is enforced with a `static_assert`. + //! //! @param arg A reference to a const `any`. //! @return A reference to the v-table pointer for the bound value. static auto vptr(const boost::type_erasure::any& arg) -> const vptr_type& { + detail::assert_std_rtti_type_erasure(); return Registry::vptr::vptr(&boost::type_erasure::typeid_of(arg)); } @@ -363,10 +394,15 @@ struct virtual_traits, Registry> { //! Looks up the @ref type_id returned by //! `boost::type_erasure::typeid_of` in the registry's `vptr` policy. //! + //! This requires the registry's @ref rtti policy to derive from + //! @ref std_rtti, which identifies classes by `&typeid(T)`, as + //! `typeid_of` does; the requirement is enforced with a `static_assert`. + //! //! @param arg A reference to a const `any`. //! @return A reference to the v-table pointer for the bound value. static auto vptr(const boost::type_erasure::any& arg) -> const vptr_type& { + detail::assert_std_rtti_type_erasure(); return Registry::vptr::vptr(&boost::type_erasure::typeid_of(arg)); } @@ -429,10 +465,15 @@ struct virtual_traits, Registry> { //! Looks up the @ref type_id returned by //! `boost::type_erasure::typeid_of` in the registry's `vptr` policy. //! + //! This requires the registry's @ref rtti policy to derive from + //! @ref std_rtti, which identifies classes by `&typeid(T)`, as + //! `typeid_of` does; the requirement is enforced with a `static_assert`. + //! //! @param arg A reference to a const `any`. //! @return A reference to the v-table pointer for the bound value. static auto vptr(const boost::type_erasure::any& arg) -> const vptr_type& { + detail::assert_std_rtti_type_erasure(); return Registry::vptr::vptr(&boost::type_erasure::typeid_of(arg)); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 70329e76..bb7a7d91 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -184,6 +184,8 @@ openmethod_compile_fail_test( "do not wrap an object that has a boost_openmethod_vptr overload") openmethod_compile_fail_test( compile_fail_std_any_custom_rtti "requires standard RTTI") +openmethod_compile_fail_test( + compile_fail_type_erasure_custom_rtti "requires standard RTTI") if (TARGET Boost::dll) add_subdirectory(dynamic_loading) diff --git a/test/compile_fail_type_erasure_custom_rtti.cpp b/test/compile_fail_type_erasure_custom_rtti.cpp new file mode 100644 index 00000000..000d3112 --- /dev/null +++ b/test/compile_fail_type_erasure_custom_rtti.cpp @@ -0,0 +1,69 @@ +// Copyright (c) 2018-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include + +#include +#include + +#include +#include + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +template +struct type_tag { + static constexpr char id = 0; +}; + +// A complete rtti policy that identifies classes by the address of a per-class +// static variable, rather than by `&typeid(T)`. Nothing else in the library +// objects to it - only the type_erasure interop does. +struct custom_rtti : policies::rtti { + template + struct fn : defaults { + template + static constexpr bool is_polymorphic = false; + + template + static auto static_type() -> type_id { + return &type_tag::id; + } + + template + static auto dynamic_type(const T&) -> type_id { + return &type_tag::id; + } + }; +}; + +struct custom_rtti_registry + : default_registry::with::without {}; + +using Concept = + boost::mpl::vector, te::typeid_<>, te::relaxed>; +using erased = te::any; + +struct Dog { + std::string name; +}; + +BOOST_OPENMETHOD_REGISTER( + use_type_erasure_types); + +// Dispatching on a type_erasure::any keys on the `std::type_info` returned by +// `boost::type_erasure::typeid_of`, so the registry's rtti policy must identify +// classes the same way. This one does not: the lookup key would be meaningless, +// and `type_id` being `const void*`, the call would otherwise compile silently. +BOOST_OPENMETHOD( + name, (virtual_), std::string, custom_rtti_registry); + +int main() { + // Call the method: declaring it is not enough to instantiate it on + // every compiler, and the guard lives in `virtual_traits::vptr`. + erased dog = Dog{"Snoopy"}; + return name(dog).size(); +} From 6730af4a6150c31d77996c0e28556c526b9a83be Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 16 Aug 2026 13:30:35 -0400 Subject: [PATCH 60/64] doc: state the any interop's rtti requirement as what, not how "the rtti policy must derive from std_rtti" describes how the check is implemented. What is required is that the policy be std_rtti. Say that, in both any headers. Co-Authored-By: Claude Opus 5 --- .../boost/openmethod/interop/boost_any.hpp | 21 ++++++++----------- include/boost/openmethod/interop/std_any.hpp | 15 ++++++------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/include/boost/openmethod/interop/boost_any.hpp b/include/boost/openmethod/interop/boost_any.hpp index 6456de02..1e639633 100644 --- a/include/boost/openmethod/interop/boost_any.hpp +++ b/include/boost/openmethod/interop/boost_any.hpp @@ -65,10 +65,9 @@ struct virtual_traits { //! //! Acquires the dynamic @ref type_id of the value stored in `arg`, using //! `boost::any::type()`. This requires the registry's @ref rtti policy to - //! identify classes by `&typeid(T)`, as @ref std_rtti does; - //! `boost::any::type()` yields the same `std::type_info` object, provided - //! Boost.TypeIndex uses `stl_type_index`. The requirement is enforced with - //! a `static_assert`. + //! be @ref std_rtti; the requirement is enforced with a `static_assert`. + //! `boost::any::type()` yields the same `std::type_info` object as + //! `&typeid(T)`, provided Boost.TypeIndex uses `stl_type_index`. //! //! Passes the type id to the registry's @ref policies::vptr policy, which //! must provide @ref policies::VptrFn::vptr. Both @@ -142,10 +141,9 @@ struct virtual_traits { //! //! Acquires the dynamic @ref type_id of the value stored in `arg`, using //! `boost::any::type()`. This requires the registry's @ref rtti policy to - //! identify classes by `&typeid(T)`, as @ref std_rtti does; - //! `boost::any::type()` yields the same `std::type_info` object, provided - //! Boost.TypeIndex uses `stl_type_index`. The requirement is enforced with - //! a `static_assert`. + //! be @ref std_rtti; the requirement is enforced with a `static_assert`. + //! `boost::any::type()` yields the same `std::type_info` object as + //! `&typeid(T)`, provided Boost.TypeIndex uses `stl_type_index`. //! //! Passes the type id to the registry's @ref policies::vptr policy, which //! must provide @ref policies::VptrFn::vptr. Both @@ -219,10 +217,9 @@ struct virtual_traits { //! //! Acquires the dynamic @ref type_id of the value stored in `arg`, using //! `boost::any::type()`. This requires the registry's @ref rtti policy to - //! identify classes by `&typeid(T)`, as @ref std_rtti does; - //! `boost::any::type()` yields the same `std::type_info` object, provided - //! Boost.TypeIndex uses `stl_type_index`. The requirement is enforced with - //! a `static_assert`. + //! be @ref std_rtti; the requirement is enforced with a `static_assert`. + //! `boost::any::type()` yields the same `std::type_info` object as + //! `&typeid(T)`, provided Boost.TypeIndex uses `stl_type_index`. //! //! Passes the type id to the registry's @ref policies::vptr policy, which //! must provide @ref policies::VptrFn::vptr. Both diff --git a/include/boost/openmethod/interop/std_any.hpp b/include/boost/openmethod/interop/std_any.hpp index a5da087d..0962ab31 100644 --- a/include/boost/openmethod/interop/std_any.hpp +++ b/include/boost/openmethod/interop/std_any.hpp @@ -64,9 +64,8 @@ struct virtual_traits { //! Returns a *reference* to a v-table pointer for an object. //! //! Acquires the @ref type_id of the value stored in `arg`, using - //! `std::any::type()`. This requires the registry's @ref rtti policy to - //! derive from @ref std_rtti, which identifies classes by `&typeid(T)`; - //! the requirement is enforced with a `static_assert`. + //! `std::any::type()`. This requires the registry's @ref rtti policy to be + //! @ref std_rtti; the requirement is enforced with a `static_assert`. //! //! Passes it to the registry's @ref policies::vptr policy, which must //! provide @ref policies::VptrFn::vptr. Both @ref policies::vptr_vector @@ -131,9 +130,8 @@ struct virtual_traits { //! Returns a *reference* to a v-table pointer for an object. //! //! Acquires the @ref type_id of the value stored in `arg`, using - //! `std::any::type()`. This requires the registry's @ref rtti policy to - //! derive from @ref std_rtti, which identifies classes by `&typeid(T)`; - //! the requirement is enforced with a `static_assert`. + //! `std::any::type()`. This requires the registry's @ref rtti policy to be + //! @ref std_rtti; the requirement is enforced with a `static_assert`. //! //! Passes it to the registry's @ref policies::vptr policy, which must //! provide @ref policies::VptrFn::vptr. Both @ref policies::vptr_vector @@ -199,9 +197,8 @@ struct virtual_traits { //! Returns a *reference* to a v-table pointer for an object. //! //! Acquires the @ref type_id of the value stored in `arg`, using - //! `std::any::type()`. This requires the registry's @ref rtti policy to - //! derive from @ref std_rtti, which identifies classes by `&typeid(T)`; - //! the requirement is enforced with a `static_assert`. + //! `std::any::type()`. This requires the registry's @ref rtti policy to be + //! @ref std_rtti; the requirement is enforced with a `static_assert`. //! //! Passes it to the registry's @ref policies::vptr policy, which must //! provide @ref policies::VptrFn::vptr. Both @ref policies::vptr_vector From f298c6f10ad84a78f72ef920e87d82e6b20ff89e Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 16 Aug 2026 13:31:56 -0400 Subject: [PATCH 61/64] doc: state the type_erasure rtti requirement as what, not how Match the any headers: the requirement is that the rtti policy be std_rtti, not that it derive from it - derivation is how the check is implemented. Co-Authored-By: Claude Opus 5 --- .../ROOT/pages/interop_type_erasure.adoc | 15 ++++++----- .../openmethod/interop/boost_type_erasure.hpp | 25 ++++++++----------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/doc/modules/ROOT/pages/interop_type_erasure.adoc b/doc/modules/ROOT/pages/interop_type_erasure.adoc index 8702bf88..3a78e792 100644 --- a/doc/modules/ROOT/pages/interop_type_erasure.adoc +++ b/doc/modules/ROOT/pages/interop_type_erasure.adoc @@ -15,14 +15,13 @@ is not included by ``, so it must be included explicitly. Dispatch resolves on the type returned by `boost::type_erasure::typeid_of`, so the ``any``'s concept must include `boost::type_erasure::typeid_<>`. -`typeid_of` returns a `std::type_info`, so the registry's `rtti` policy must -also identify classes that way: it must be cpp:std_rtti[], or a policy derived -from it. `default_registry` and `indirect_registry` both qualify. A registry -with, say, cpp:static_rtti[] identifies classes by a different kind of -`type_id`, and would look up the wrong v-table; the requirement is enforced with -a `static_assert`. The cpp:openmethod_vptr[] concept takes the v-table pointer -from the ``any``'s own dispatch table and never calls `typeid_of`, so it does -not require `std_rtti`. +`typeid_of` returns a `std::type_info`, so the registry's `rtti` policy must be +cpp:std_rtti[], or a policy derived from it. `default_registry` and +`indirect_registry` both qualify. A registry with, say, cpp:static_rtti[] +identifies classes by a different kind of `type_id`, and would look up the wrong +v-table; the requirement is enforced with a `static_assert`. The +cpp:openmethod_vptr[] concept takes the v-table pointer from the ``any``'s own +dispatch table and never calls `typeid_of`, so it does not require `std_rtti`. The types an `any` may bind to must be registered via cpp:use_type_erasure_types[]. ``any``s with different concepts must each be diff --git a/include/boost/openmethod/interop/boost_type_erasure.hpp b/include/boost/openmethod/interop/boost_type_erasure.hpp index e8742f6c..4954d10d 100644 --- a/include/boost/openmethod/interop/boost_type_erasure.hpp +++ b/include/boost/openmethod/interop/boost_type_erasure.hpp @@ -174,9 +174,8 @@ struct virtual_traits&, Registry> { //! Looks up the @ref type_id returned by //! `boost::type_erasure::typeid_of` in the registry's `vptr` policy. //! - //! This requires the registry's @ref rtti policy to derive from - //! @ref std_rtti, which identifies classes by `&typeid(T)`, as - //! `typeid_of` does; the requirement is enforced with a `static_assert`. + //! This requires the registry's @ref rtti policy to be @ref std_rtti; + //! the requirement is enforced with a `static_assert`. //! //! @param arg A reference to a const `any`. //! @return A reference to the v-table pointer for the bound value. @@ -244,9 +243,8 @@ struct virtual_traits&, Registry> { //! Looks up the @ref type_id returned by //! `boost::type_erasure::typeid_of` in the registry's `vptr` policy. //! - //! This requires the registry's @ref rtti policy to derive from - //! @ref std_rtti, which identifies classes by `&typeid(T)`, as - //! `typeid_of` does; the requirement is enforced with a `static_assert`. + //! This requires the registry's @ref rtti policy to be @ref std_rtti; + //! the requirement is enforced with a `static_assert`. //! //! @param arg A reference to an `any`. //! @return A reference to the v-table pointer for the bound value. @@ -314,9 +312,8 @@ struct virtual_traits&&, Registry> { //! Looks up the @ref type_id returned by //! `boost::type_erasure::typeid_of` in the registry's `vptr` policy. //! - //! This requires the registry's @ref rtti policy to derive from - //! @ref std_rtti, which identifies classes by `&typeid(T)`, as - //! `typeid_of` does; the requirement is enforced with a `static_assert`. + //! This requires the registry's @ref rtti policy to be @ref std_rtti; + //! the requirement is enforced with a `static_assert`. //! //! @param arg A reference to a const `any`. //! @return A reference to the v-table pointer for the bound value. @@ -394,9 +391,8 @@ struct virtual_traits, Registry> { //! Looks up the @ref type_id returned by //! `boost::type_erasure::typeid_of` in the registry's `vptr` policy. //! - //! This requires the registry's @ref rtti policy to derive from - //! @ref std_rtti, which identifies classes by `&typeid(T)`, as - //! `typeid_of` does; the requirement is enforced with a `static_assert`. + //! This requires the registry's @ref rtti policy to be @ref std_rtti; + //! the requirement is enforced with a `static_assert`. //! //! @param arg A reference to a const `any`. //! @return A reference to the v-table pointer for the bound value. @@ -465,9 +461,8 @@ struct virtual_traits, Registry> { //! Looks up the @ref type_id returned by //! `boost::type_erasure::typeid_of` in the registry's `vptr` policy. //! - //! This requires the registry's @ref rtti policy to derive from - //! @ref std_rtti, which identifies classes by `&typeid(T)`, as - //! `typeid_of` does; the requirement is enforced with a `static_assert`. + //! This requires the registry's @ref rtti policy to be @ref std_rtti; + //! the requirement is enforced with a `static_assert`. //! //! @param arg A reference to a const `any`. //! @return A reference to the v-table pointer for the bound value. From 20a2a963ba5cb82f603bca0a23defc8e393cdf13 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 16 Aug 2026 13:38:25 -0400 Subject: [PATCH 62/64] doc, test: openmethod_vptr lifts the std_rtti requirement The concept takes the v-table pointer from the any's own dispatch table, so dispatch never calls typeid_of. The registry then needs an rtti policy only for the static type identification initialize() performs, and needs neither a vptr policy nor the type_hash one would depend on: registry suffices. Mention it as the counterweight to the coupling the concept imposes, and cover it with a test so the guarantee cannot regress silently. Co-Authored-By: Claude Opus 5 --- .../ROOT/pages/interop_type_erasure.adoc | 8 +++ test/test_type_erasure_static_rtti.cpp | 66 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 test/test_type_erasure_static_rtti.cpp diff --git a/doc/modules/ROOT/pages/interop_type_erasure.adoc b/doc/modules/ROOT/pages/interop_type_erasure.adoc index 3a78e792..54bcd954 100644 --- a/doc/modules/ROOT/pages/interop_type_erasure.adoc +++ b/doc/modules/ROOT/pages/interop_type_erasure.adoc @@ -162,6 +162,14 @@ works with any pre-existing Concept containing `typeid_<>`. To use an `any` with several registries, list the concept several times, once per registry: `openmethod_vptr`. +In exchange, the requirement for cpp:std_rtti[] goes away. The v-table pointer +comes from the ``any``'s own dispatch table, so the registry's `rtti` policy is +needed only to identify classes when `initialize()` builds the dispatch tables - +cpp:static_rtti[] is enough. No hashing is involved either: the registry needs +neither a `vptr` policy nor any policy that one depends on, like `type_hash`. +`registry` will do. Note that this does not make the +program RTTI-free: Boost.TypeErasure itself uses `typeid`. + #### Empty ``any``s An empty relaxed `any` reports `typeid(void)`, which is not a registered diff --git a/test/test_type_erasure_static_rtti.cpp b/test/test_type_erasure_static_rtti.cpp new file mode 100644 index 00000000..610fa503 --- /dev/null +++ b/test/test_type_erasure_static_rtti.cpp @@ -0,0 +1,66 @@ +// Copyright (c) 2018-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include + +#include +#include + +#include + +#include +#include +#include + +#define BOOST_TEST_MODULE openmethod +#include + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +// The `openmethod_vptr` concept takes the v-table pointer from the `any`'s own +// dispatch table, so it does not go through `typeid_of`, and the `std_rtti` +// requirement that the `typeid_of`-based traits assert does not apply. The +// registry then needs an rtti policy only for the static type identification +// `initialize()` performs - `static_rtti` suffices - and neither a `vptr` +// policy nor the `type_hash` one would depend on. +struct minimal_registry : registry {}; + +struct Dog { + std::string name; +}; + +struct Cat { + std::string name; +}; + +struct Dispatchable : boost::mpl::vector< + te::copy_constructible<>, te::relaxed, + openmethod_vptr> {}; + +using erased = te::any; + +// Binding a value to the `any` registers its type; no use_type_erasure_types. + +BOOST_OPENMETHOD( + name, (virtual_), std::string, minimal_registry); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const Cat& cat), std::string) { + return cat.name + " the cat"; +} + +BOOST_AUTO_TEST_CASE(type_erasure_openmethod_vptr_needs_no_std_rtti) { + initialize(); + + const erased spot(Dog{"Spot"}); + const erased tom(Cat{"Tom"}); + + BOOST_TEST(name(spot) == "Spot the dog"); + BOOST_TEST(name(tom) == "Tom the cat"); +} From 7018cafa506283d208b329ab3c9f2b1c05ea1816 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Mon, 17 Aug 2026 11:29:27 -0400 Subject: [PATCH 63/64] any: document IsVirtualAny, and test that it rejects a handle Turn detail::is_virtual_any_aux into IsVirtualAny, a variable template inside the OPEN/CLOSE_NAMESPACE_DETAIL_UNLESS_MRDOCS block, so MrDocs documents it as exposition only, the way IsPolymorphic, IsSmartPtr and SameSmartPtr already are. The PascalCase name follows those; the block puts it in namespace detail for every compiler other than MrDocs, so the constraint it appears in resolves to a documented symbol. Add compile_fail_virtual_any_from_ref, which copy-initializes a virtual_any from a virtual_any_ref. That is ill-formed because the value constructor is constrained away and storing the handle would take two user-defined conversions. Without the virtual_any_ref specialization the value constructor accepts the handle, stores it inside the `any`, and looks up static_vptr for a type that is not a registered class, which is null: an assertion failure in a debug build, and a null v-table pointer carried to the first dispatch in a release one. msvc needs /permissive- for that file. In its default mode, which /std:c++17 does not turn off, it accepts the extra user-defined conversion and compiles the file, so the test would not fail. The b2 target is therefore spelled out rather than globbed. Note the constraint does not actually protect msvc users building in the default mode - and the direct-initialization form, `virtual_any va(ref)`, is worse: one user-defined conversion suffices there, so no compiler rejects it and the handle is stored with a null v-table pointer. Co-Authored-By: Claude Opus 5 --- .../boost/openmethod/interop/virtual_any.hpp | 44 ++++++++++++++++--- test/CMakeLists.txt | 18 ++++++++ test/Jamfile | 9 +++- test/compile_fail_virtual_any_from_ref.cpp | 34 ++++++++++++++ 4 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 test/compile_fail_virtual_any_from_ref.cpp diff --git a/include/boost/openmethod/interop/virtual_any.hpp b/include/boost/openmethod/interop/virtual_any.hpp index f34491a6..c2b0c58a 100644 --- a/include/boost/openmethod/interop/virtual_any.hpp +++ b/include/boost/openmethod/interop/virtual_any.hpp @@ -19,18 +19,46 @@ class virtual_any; template class virtual_any_ref; -namespace detail { +BOOST_OPENMETHOD_OPEN_NAMESPACE_DETAIL_UNLESS_MRDOCS +//! Test if argument is a wide `any` (exposition only) +//! +//! Evaluates to `true` if `T` is a specialization of @ref virtual_any or of +//! @ref virtual_any_ref, and `false` otherwise. +//! +//! This constrains the constructor and the assignment operator of +//! @ref virtual_any that take a value, excluding both wide types - every +//! specialization of them, not only the ones matching this `virtual_any`. A +//! @ref virtual_any argument then selects the copy or move operation instead +//! of being stored inside the `any`, and a @ref virtual_any_ref argument is +//! rejected outright rather than stored: a handle is not a registered class, +//! so its @ref registry::static_vptr would be null. +//! +//! @tparam T A type. template -struct is_virtual_any_aux : std::false_type {}; +constexpr bool IsVirtualAny = false; +//! Recognize a virtual_any (exposition only) +//! +//! The specialization of @ref IsVirtualAny that matches a +//! `virtual_any`, and evaluates to `true`. +//! +//! @tparam Any An `any` type. +//! @tparam Registry A @ref registry. template -struct is_virtual_any_aux> : std::true_type {}; +constexpr bool IsVirtualAny> = true; +//! Recognize a virtual_any_ref (exposition only) +//! +//! The specialization of @ref IsVirtualAny that matches a +//! `virtual_any_ref`, and evaluates to `true`. +//! +//! @tparam Any An `any` type, possibly const-qualified. +//! @tparam Registry A @ref registry. template -struct is_virtual_any_aux> : std::true_type {}; +constexpr bool IsVirtualAny> = true; -} // namespace detail +BOOST_OPENMETHOD_CLOSE_NAMESPACE_DETAIL_UNLESS_MRDOCS //! A wide `any`, combining an `any` and a pointer to a v-table. //! @@ -128,7 +156,8 @@ class virtual_any { template< typename T, typename = std::enable_if_t< - !detail::is_virtual_any_aux>::value && + !BOOST_OPENMETHOD_DETAIL_UNLESS_MRDOCS + IsVirtualAny> && !std::is_same_v, Any> && std::is_constructible_v>> virtual_any(T&& value) @@ -203,7 +232,8 @@ class virtual_any { template< typename T, typename = std::enable_if_t< - !detail::is_virtual_any_aux>::value && + !BOOST_OPENMETHOD_DETAIL_UNLESS_MRDOCS + IsVirtualAny> && !std::is_same_v, Any> && std::is_constructible_v>> auto operator=(T&& value) -> virtual_any& { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 15759641..912c39e0 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -168,6 +168,24 @@ openmethod_compile_fail_test( openmethod_compile_fail_test( compile_fail_virtual_any_ref_by_ref "virtual_any_ref is a cheap handle, pass it by value") +# Copy-initializing a virtual_any from a virtual_any_ref is ill-formed: the +# value constructor is constrained away, so storing the handle inside the +# `any` would take two user-defined conversions. The diagnostic is the +# compiler's own, and the wording varies: "conversion from ... to non-scalar +# type ... requested" on gcc, "no viable conversion from" on clang, C2440 +# "cannot convert from" on MSVC. +# +# MSVC needs /permissive- here. In its default mode - which `/std:c++17` does +# not turn off - it accepts the extra user-defined conversion and compiles the +# file, so the test would not fail. Note this means the constraint does not +# actually protect MSVC users building in the default mode; only the +# direct-initialization form, which no compiler rejects, is worse. +openmethod_compile_fail_test( + compile_fail_virtual_any_from_ref "conversion from|cannot convert from") +if (MSVC) + target_compile_options( + boost_openmethod-compile_fail_virtual_any_from_ref PRIVATE /permissive-) +endif() # "use of a deleted function" on gcc, "call to deleted function" on clang, # "attempting to reference a deleted function" on MSVC. openmethod_compile_fail_test( diff --git a/test/Jamfile b/test/Jamfile index 10ab8c56..b67ca9fd 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -42,11 +42,18 @@ for local src in [ glob test_*.cpp ] run mix_release_debug/main.cpp mix_release_debug/lib.cpp unit_test_framework ; -for local src in [ glob compile_fail_*.cpp ] +for local src in [ glob compile_fail_*.cpp : compile_fail_virtual_any_from_ref.cpp ] { compile-fail $(src) ; } +# Excluded from the glob above because it needs /permissive- on msvc: copy- +# initializing a virtual_any from a virtual_any_ref takes two user-defined +# conversions, which msvc accepts in its default mode, so the compile would +# succeed and the test fail. See test/CMakeLists.txt for the details. +compile-fail compile_fail_virtual_any_from_ref.cpp + : msvc:/permissive- ; + build-project dynamic_loading ; build-project implicit_shared_libraries ; diff --git a/test/compile_fail_virtual_any_from_ref.cpp b/test/compile_fail_virtual_any_from_ref.cpp new file mode 100644 index 00000000..5513417d --- /dev/null +++ b/test/compile_fail_virtual_any_from_ref.cpp @@ -0,0 +1,34 @@ +// Copyright (c) 2018-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +#include + +#include +#include + +using namespace boost::openmethod; + +struct Dog { + std::string name; +}; + +BOOST_OPENMETHOD_REGISTER(use_std_any_types); + +int main() { + std::any dog(Dog{"Snoopy"}); + virtual_any_ref ref(dog); + + // A virtual_any cannot be copy-initialized from a virtual_any_ref. The + // value constructor is constrained to reject every wide type, so storing + // the handle inside the `any` would take two user-defined conversions - + // virtual_any_ref to std::any, then std::any to virtual_any - which is + // one more than an implicit conversion sequence allows. Were the handle + // stored, its static_vptr would be null: a handle is not a registered + // class. + virtual_std_any copy = ref; + + return 0; +} From 658acee66ffba207339ecf08b28e5464e3904ac5 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Mon, 17 Aug 2026 20:57:48 -0400 Subject: [PATCH 64/64] ci: empty commit to re-trigger the docs preview build The prtest3 preview for PR #86 is still the 16 Aug snapshot; the push of 378736b did not fire a preview build. Co-Authored-By: Claude Opus 5