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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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 78143c84d453e7d724891a16ed80eedc0abd6e14 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 8 Aug 2026 12:54:55 -0400 Subject: [PATCH 10/33] 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 11/33] 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 12/33] 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 13/33] 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 14/33] 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 15/33] 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 16/33] 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 17/33] 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 3bedb21abc22fa80324467316c25cda4d49a70f6 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 12:04:15 -0400 Subject: [PATCH 18/33] 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 19/33] 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 20/33] 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 21/33] 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 437c6662600b51a2d9e9952f119420aa3e02afac Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 14:50:44 -0400 Subject: [PATCH 22/33] 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 516606ce7be796e51bfed740d661253c97e5f962 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 15:13:09 -0400 Subject: [PATCH 23/33] 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 69b5d43bc6d76ba32f14ced602e15ede91a79ffd Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 17:05:37 -0400 Subject: [PATCH 24/33] 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 25/33] 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 cb44af63e6038a42fa3e045bd9ae048979b15fe9 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 9 Aug 2026 20:04:40 -0400 Subject: [PATCH 26/33] 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 27/33] 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 235344f28c690b8f9dec63fc6876ef3c8de4360f Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 15 Aug 2026 10:56:19 -0400 Subject: [PATCH 28/33] 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 1f8ed775c3cacd2b7a33cea52d1c4ef29ea84372 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 15 Aug 2026 16:44:00 -0400 Subject: [PATCH 29/33] 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 a01efedc2b0e29e6b86158027c1aad92204b7048 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 16 Aug 2026 11:11:16 -0400 Subject: [PATCH 30/33] 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 49bfbf5924ce11d9a206f54b6c6300e03e73ec43 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 16 Aug 2026 13:10:00 -0400 Subject: [PATCH 31/33] 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 6730af4a6150c31d77996c0e28556c526b9a83be Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 16 Aug 2026 13:30:35 -0400 Subject: [PATCH 32/33] 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 7018cafa506283d208b329ab3c9f2b1c05ea1816 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Mon, 17 Aug 2026 11:29:27 -0400 Subject: [PATCH 33/33] 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; +}