Skip to content

interop with Boost.TypeErasure - #86

Open
jll63 wants to merge 83 commits into
boostorg:developfrom
jll63:feature/type_erasure
Open

interop with Boost.TypeErasure#86
jll63 wants to merge 83 commits into
boostorg:developfrom
jll63:feature/type_erasure

Conversation

@jll63

@jll63 jll63 commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

jll63 and others added 28 commits March 6, 2026 16:29
# Conflicts:
#	include/boost/openmethod/policies/vptr_map.hpp
#	include/boost/openmethod/preamble.hpp
Add virtual_traits<std::any&>, and test dispatch on a std::any passed by
mutable lvalue reference and by xvalue reference.

virtual_<std::any&> silently bound the generic virtual_traits<Class&>,
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<std::any&&>::cast passed its parameter to std::any_cast
as an lvalue, selecting the any_cast(any&) overload, which asserts
is_constructible_v<U, _Up&> - 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<Fn> instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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 <noreply@anthropic.com>
virtual_any<Any, Registry> 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<const Any&>::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<Any cvref>
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<Fn>, as with virtual_<Any&>.

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 <T, T> one, which partial ordering ranks neither above
nor below <virtual_any cvref, T2>.

The class is generic: it only requires virtual_traits<Any cvref> 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<any> - the v-table of the `any` root class, not of the
contained value.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
acquire_vptr and method::vptr detected a traits-supplied vptr with
has_vptr<virtual_traits<...>, type_id>, i.e. by asking whether
traits::vptr is callable with a type_id (a const void*). The member takes
a reference to the any, so the probe only passed because std::any and
boost::any happen to have a greedy converting constructor that accepts a
const void*. An any-like type without such a constructor would silently
fail the probe and fall through to the vptr policy's dynamic_vptr, which
keys the lookup on typeid(wrapper) - the wrapper class itself, not the
contained value.

Probe with the actual argument type instead, making the detection ask
the intended question: does this specialization provide a vptr member.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An overrider may take the method's `any` parameter itself, acting as a
catch-all for contained types that have no more specific overrider. The
virtual_traits cast<U> members passed U to any_cast unconditionally, and
any_cast to the any's own type throws unless the any contains an any.
Return the argument unchanged when U is the any, by value or by any
reference category - as virtual_any's traits already did.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The registrars expanded their whole template parameter pack into
use_class_aux instantiations, so a trailing registry argument - accepted,
and used to select the registry - was also registered as a class derived
from the any root. Harmless, but wrong. Factor the expansion into
detail::use_any_types_aux (in virtual_any.hpp, shared by all the any
interop headers), driven by extract_registry's `others` list, which
excludes the registry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add interop/boost_type_erasure.hpp: dispatch on the type bound to a
boost::type_erasure::any, via virtual_traits and the vptr policies'
type-id-keyed entry point - the same approach as the std::any and
boost::any interop, with no custom rtti policy or registry. Dispatch
keys on the std::type_info returned by typeid_of, so the only
requirement on the user's Concept is typeid_<>, which `relaxed` already
implies.

virtual_traits specializations, generic over the Concept, cover the
owning flavor by const, mutable and rvalue reference, and the
reference-wrapper flavors (any<C, _self&>, any<C, const _self&>) by
value - they are cheap, two-word handles, and te's idiomatic parameter
carriers. All use the owning flavor as their virtual_type, so a single
registered root per Concept serves every parameter form; overriders
receive the bound type by a reference of a compatible category, or the
any itself as a catch-all. type_erasure's any_cast has no rvalue
overload, so the xvalue trait moves the result of a mutable-reference
cast - for the owning flavor only, since the rvalue-ness of a reference
wrapper says nothing about the referent's ownership. Casts that cannot
work (mutable access to const-bound values, moving out of borrowed
referents) are removed from the overload set, mirroring the boost::any
constraints.

use_type_erasure_types<Any, T...> registers the bound types under the
Concept's root, normalizing Any to the owning flavor. virtual_any
composes with no extra code: virtual_any<any<Concept>> looks the v-table
pointer up once, at construction - recovering O(1) vptr acquisition,
which the concept-interface-injection approach sketched in boostorg#21 obtained
at the cost of naming the policy inside the user's Concept. The
final_virtual_ptr overloads for type_erasure::any are deleted: the
primary would silently use the root's static v-table pointer.

Dispatch on the reference flavors is on the type bound at construction,
never the C++ RTTI dynamic type of the referent; an empty relaxed any
yields typeid(void), reported as missing_class under runtime checks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
`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_<const std::any&>` 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
`virtual_traits<const virtual_any&>::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_<const std::any&>` 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 <noreply@anthropic.com>
The page opened on `virtual_std_any`, which put the wrapper - an optimization -
before the plain thing it optimizes. Lead with `virtual_<const std::any&>`
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 <noreply@anthropic.com>
…n 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
# Conflicts:
#	include/boost/openmethod/interop/std_any.hpp
#	test/test_dispatch_std_any.cpp
…mples

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jll63 jll63 linked an issue Aug 8, 2026 that may be closed by this pull request
@cppalliance-bot

cppalliance-bot commented Aug 8, 2026

Copy link
Copy Markdown

An automated preview of the documentation is available at https://86.openmethod.prtest3.cppalliance.org/libs/openmethod/doc/html/index.html

If more commits are pushed to the pull request, the docs will rebuild at the same URL.

2026-08-16 17:47:38 UTC

jll63 and others added 30 commits August 10, 2026 01:44
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MSVC's /std:c++17 does not imply /permissive-, so it injects hidden
friends into the enclosing namespace, where ordinary lookup finds them.
The unconstrained `const derived<Base>::type&` parameter then made the
hook a candidate for an `any` over an unrelated Concept: MSVC tried to
convert one flavor to the other, instantiating TypeErasure's binding
converting constructor, which fails outside the immediate context --
an error, not a substitution failure.

    boost/type_erasure/any.hpp(2054): error C2661:
      'binding<Concept>::binding': no overloaded function takes 2 arguments

Deduce the parameter and require an exact match, as virtual_any already
does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The test declared the method but never called it. GCC, clang and
msvc-14.3 instantiate `method<...>` at the declaration, so the guard
fired; MSVC v18 defers, and the test compiled clean - a compile-fail
test that no longer fails. Call the method, as the virtual_any
by-value test does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Catalina runner is the slowest in the fleet: at the observed rate the
UBSAN and ASAN stages needed ~105 minutes for 17,2a, and both were killed
(exit 137) at Drone's 60-minute step timeout partway into the second
standard. Give each standard its own stage, as the GCC 13 Linux stages
already do for the same reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
virtual_any over a boost::type_erasure::any was supported - the header
lists the form among the supported virtual parameters - but had neither
a test nor documentation.

Add test/test_virtual_any_type_erasure.cpp: the three reference
categories, value semantics, virtual_any_ref (also previously
uncovered), and indirect vptrs.

Document virtual_any in interop_type_erasure.adoc, in a new subsection
before the openmethod_vptr one, with a new example. Open the
openmethod_vptr section by presenting the concept as the alternative:
same goal, reached from the other side, and mutually exclusive with
virtual_any.

Fix a silent AsciiDoc bug while there: `word`'s is parsed as a code
span followed by the `' curly-apostrophe shorthand, which eats the
closing backtick, so the next code span in the paragraph loses its
formatting and a literal backtick reaches the output. Reword the two
pre-existing occurrences, and record the trap in CLAUDE.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Move the Steven Watanabe credit out of the openmethod_vptr section,
where it read as being about that concept alone, into an
Acknowledgment section at the end of each interop page, covering the
interop as a whole.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow the Acknowledgment sections added to the interop pages: credit
the interop as a whole rather than the openmethod_vptr concept alone.

In boost_type_erasure.hpp, drop the parenthetical from the file
comment and the credit from the openmethod_vptr doc comment - which
rendered onto its reference page - and state it once, at the end of
the file comment. Give std_any.hpp and boost_any.hpp, which had no
file comment at all, the same line. virtual_any.hpp is left alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Conflict in the four virtual_ptr ctor/assign doc comments: this branch no
longer routes virtual_ptr through boost_openmethod_vptr. Kept this branch's
wording, and reworked the incoming reference page to match: the hook is for
virtual_ parameters only, and wrapping an object that provides one in a
virtual_ptr is rejected at compile time (acquire_vptr's static_assert).
Dropped the paragraph the merge brought into virtual_ptr_alt.adoc, which
claimed virtual_ptr honors the hook.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Use Boost.TypeErasure's own vocabulary. "flavor" appears nowhere in its
docs or headers; the noun for `any<Concept, _self&>` is "any reference",
and what distinguishes it from `any<Concept>` is the *placeholder*. So:
"reference flavors" and "reference-wrapper" become "any references",
"the rvalue-reference flavor" becomes "the rvalue-reference placeholder",
and "whatever the flavor of the parameter" becomes "whatever the
placeholder". "owning any" stays - "owning" is ours, but the distinction
is real and the term reads clearly.

Keep "wrapper" where it means virtual_any, which is one of ours, and
where it means the const-qualified any rather than a reference.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	doc/modules/ROOT/pages/virtual_ptr_alt.adoc
"Borrowing" is Rust terminology. Say that virtual_any_ref refers to an
existing any, and that the any reference's referent is not owned.

Also reflow a paragraph in interop_any.adoc left ragged by the earlier
rewording.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Dispatching on a type_erasure::any keys on the std::type_info returned by
boost::type_erasure::typeid_of, so the registry's rtti policy must
identify classes the same way. Assert it in virtual_traits::vptr, as for
std::any and boost::any.

The openmethod_vptr concept takes the vptr from the any's own dispatch
table and never calls typeid_of, so it is deliberately not covered: the
assert lives in the vptr body rather than at class scope, and does not
fire for that path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"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 <noreply@anthropic.com>
Match the any headers: the requirement is that the rtti policy be
std_rtti, not that it derive from it - derivation is how the check is
implemented.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The concept takes the v-table pointer from the any's own dispatch table,
so dispatch never calls typeid_of. The registry then needs an rtti policy
only for the static type identification initialize() performs, and needs
neither a vptr policy nor the type_hash one would depend on:
registry<policies::static_rtti> suffices.

Mention it as the counterweight to the coupling the concept imposes, and
cover it with a test so the guarantee cannot regress silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

interop with TypeErasure

2 participants