Skip to content

interop with Boost.TypeErasure - #86

Merged
jll63 merged 109 commits into
boostorg:developfrom
jll63:feature/type_erasure
Aug 22, 2026
Merged

interop with Boost.TypeErasure#86
jll63 merged 109 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-22 17:53:43 UTC

jll63 and others added 3 commits August 16, 2026 13:38
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>
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<std::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 <noreply@anthropic.com>
# Conflicts:
#	include/boost/openmethod/interop/virtual_any.hpp
@codecov

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.47328% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.97%. Comparing base (a4b84b0) to head (0b285a8).
⚠️ Report is 10 commits behind head on develop.

Files with missing lines Patch % Lines
test/test_dispatch_boost_any.cpp 96.57% 5 Missing ⚠️
test/test_dispatch_std_any.cpp 96.57% 5 Missing ⚠️
test/test_virtual_any_boost.cpp 98.26% 2 Missing ⚠️
test/test_virtual_any_std.cpp 98.26% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop      #86      +/-   ##
===========================================
+ Coverage    94.31%   94.97%   +0.66%     
===========================================
  Files           88       99      +11     
  Lines         3325     4322     +997     
  Branches      1579     2138     +559     
===========================================
+ Hits          3136     4105     +969     
- Misses         158      160       +2     
- Partials        31       57      +26     
Files with missing lines Coverage Δ
include/boost/openmethod/core.hpp 93.06% <100.00%> (-1.04%) ⬇️
include/boost/openmethod/inplace_vptr.hpp 100.00% <ø> (ø)
include/boost/openmethod/interop/boost_any.hpp 100.00% <100.00%> (ø)
...de/boost/openmethod/interop/boost_type_erasure.hpp 100.00% <100.00%> (ø)
include/boost/openmethod/interop/std_any.hpp 100.00% <100.00%> (ø)
include/boost/openmethod/interop/virtual_any.hpp 100.00% <100.00%> (ø)
include/boost/openmethod/policies/vptr_map.hpp 100.00% <100.00%> (ø)
include/boost/openmethod/policies/vptr_vector.hpp 100.00% <100.00%> (ø)
include/boost/openmethod/preamble.hpp 75.92% <ø> (+0.45%) ⬆️
test/test_core.cpp 73.91% <ø> (-5.40%) ⬇️
... and 7 more

... and 4 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 4562ce4...0b285a8. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

jll63 and others added 24 commits August 17, 2026 20:57
The prtest3 preview for PR boostorg#86 is still the 16 Aug snapshot; the push of
378736b did not fire a preview build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comparison was to "any references" generically; virtual_any_ref
matches specifically the reference-wrapper flavors of TypeErasure's any
(any_ref/any_cref), not the value-holding any itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The handle's documentation already called it "a cheap, two-word handle
with pointer semantics", and methods take it by value - but its name said
reference, its only accessor was `get() const -> const Any&`, it had no
assignment operators, and it shared a header with the type it is the
counterpart of.

Rename it, move it to <boost/openmethod/interop/virtual_any_ptr.hpp>, and
give it virtual_ptr's shape: `element_type`, `get`/`operator->`/`pointer`
returning a pointer, `operator*`, one assignment operator per constructor,
deduction guides, and free `==`/`!=`. std_any.hpp and boost_any.hpp keep
including only virtual_any.hpp, so the handle is opt-in, like the interop
headers themselves.

The accessors are const in both instantiations. Handing out a mutable
`any&` would let the contained value be replaced behind the handle's back,
leaving it carrying the v-table pointer of the previous value - the
invariant the whole interop rests on. `Any`'s constness governs what
overriders may take, which is where that distinction already lived, in
virtual_traits::cast. The operations that do change what the handle
designates re-derive the pointer, and are safe for that reason: assignment
rebinds it, as on virtual_ptr; `emplace` replaces the contained value, as
on virtual_any, and is guarded by a static_assert for a const handle.

The rvalue assignment overloads are deleted, mirroring the constructors:
virtual_ptr's smart pointer specialization accepts rvalues because it owns
what it stores, and this does not. The two guides taking a virtual_any
deduce its registry rather than defaulting to
BOOST_OPENMETHOD_DEFAULT_REGISTRY, which virtual_ptr cannot do.

virtual_traits::cast collapses to a single virtual_traits<Any&, Registry>
call, `Any&` being `const virtual_type&` exactly when `Any` is const. It
keeps reading the private `obj`: an overrider taking `Dog&` needs the
mutable `Any&` the public accessors withhold, and confining that to one
place is the point.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Carry the virtual_any_ref -> virtual_any_ptr rename and header split into
the type_erasure interop.

boost_type_erasure.hpp keeps including only virtual_any.hpp, like std_any.hpp
and boost_any.hpp, so test_virtual_any_type_erasure.cpp - the one file on this
branch that uses the handle - now includes virtual_any_ptr.hpp itself, and its
`&spot.get() == &spot_any` becomes a plain pointer comparison. The `get()`
calls on `virtual_erased` are untouched: virtual_any still returns a reference.

Conflicts:
- interop_any.adoc: keep this branch's clarified comparison to
  Boost.TypeErasure's any references, plus the new header paragraph.
- test/CMakeLists.txt: both sides appended a compile-fail test; keep both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The header's design comment enumerates the ways an `any` can be a virtual
parameter, and stopped at `virtual_any`. The non-owning counterpart works
with `type_erasure::any` too - test_virtual_any_type_erasure.cpp covers it -
and now that it lives in its own header, which this one deliberately does not
include, the comment is where a reader would expect to be told so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Windows b2 jobs failed to build the test suite - clang-win on
libs/thread/src/win32/tss_pe.cpp ("unused variable 'dw'"), msvc-14.3 on
ARM on libs/container/src/pool_resource.cpp (C2220 via
intrusive/detail/math.hpp) - and skipped every openmethod test that
depended on the result.

Neither library is ours, and neither is one we need. The interop uses
Boost.TypeErasure's headers only; its compiled part is a single file,
dynamic_binding.cpp, which nothing here references and which links
Boost.Thread, which in turn pulls in Boost.Container, Boost.Chrono,
Boost.date_time and Boost.Atomic. `<library>` therefore built - and, on
those two toolchains, failed to build - a chain of five libraries to
link one object file we never call into.

`<use>` propagates the usage requirements, including the include path and
BOOST_TYPE_ERASURE_NO_LIB (which keeps MSVC from auto-linking), without
adding the library to the link. Boost.LEAF, Boost.Chrono and Boost.Math
consume compiled libraries header-only the same way.

Confirmed against the two branches: the ARM job on feature/any, which has
no Boost.TypeErasure dependency, builds none of thread, container,
date_time or type_erasure and passes; on feature/type_erasure the skip
chain reads type_erasure -> thread -> container. Locally, the full b2 run
now passes with none of those five libraries built.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Match what test/Jamfile now does with <use>. Boost.TypeErasure's compiled
part is one file, dynamic_binding.cpp, which nothing here references and
which links Boost::thread, so linking Boost::type_erasure built Boost.Thread
and, through it, Boost.Container, Boost.Chrono, Boost.date_time and
Boost.Atomic - five libraries for an object file we never call into.

A compile-only dependency's own compiled dependencies appear in its interface
wrapped in $<LINK_ONLY:...>, so they can be filtered out at configure time,
leaving the header-only ones whose include directories the headers do need.
That plus the dependency's include directories and compile definitions is
exactly its usage requirements minus the link. $<COMPILE_ONLY:> expresses
this in one word, but needs CMake 3.27, well past the 3.8 floor declared
here.

Boost::type_erasure stays in BOOST_OPENMETHOD_DEPENDENCIES, which is what
declares the dependency to the super-project and what drives
BOOST_INCLUDE_LIBRARIES; only the linking changes.

Measured on a clean build tree, building test_virtual_any_type_erasure:
before, 49 steps producing libboost_{atomic,chrono,container,date_time,
thread,type_erasure,unit_test_framework}; after, 25 steps producing only
libboost_unit_test_framework. The ctest set is identical either way, 155
tests pass, and the MrDocs target - which includes every header - now builds
with no Boost library compiled at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit was half a fix. <use> stopped Boost.TypeErasure itself
from being built, and the CI log confirms it - libs/type_erasure does not
appear at all any more - but Boost.Thread still was, and every openmethod
test still skipped for lack of it.

A static library cannot resolve its own dependencies, so b2 propagates them
to whoever uses it: under link=static, boost_type_erasure's usage
requirements include Boost.Thread, and <use> faithfully propagates that.
Every failing target in the CI log is a lnk-sttc variant, which is the tell.
I missed it by testing only the default link=shared, where the propagation
does not happen; CI builds link=shared,static.

Pinning the dependency to /<link>shared keeps its dependencies its own.
Nothing is built either way, since we never link it.

Verified the way the previous commit should have been: a clean run of the
full suite with link=shared,static variant=debug,release passes with zero
failures and builds neither Boost.Thread nor Boost.TypeErasure. The CMake
side already covers this case - it filters $<LINK_ONLY:...>, which is CMake's
name for the same static-propagation rule - and building with
BUILD_SHARED_LIBS=OFF likewise produces only libboost_unit_test_framework.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…gression

Two corrections to the previous two commits, both from testing the wrong
configuration.

CMake: 29dda3b broke the six cmake jobs and the cmake step of the two mingw
jobs, with "boost/type_erasure/builtin.hpp: No such file". It inspected the
dependency with get_target_property at configure time, which requires the
target to already exist. CI drives the super-project with
BOOST_INCLUDE_LIBRARIES=openmethod, and the super-project does not guarantee
it configures type_erasure before openmethod; my standalone build happened to,
which is why this passed locally and failed there. $<COMPILE_ONLY:> says the
same thing as a generator expression, so ordering cannot matter, with a plain
link below CMake 3.27. Verified in the super-project layout CI actually uses,
and the pre-3.27 branch verified by forcing it, not by assuming.

b2: cc8002d fixed clang-win but not the ARM job. <use> keeps Boost.Thread from
being *built*, but naming the target still makes b2 load Thread's Jamfile to
resolve type_erasure's requirements. That declares the threadapi feature,
which then appears in the property set of every target in the build -
thrdp-wn32 mentions go from 12 on feature/any to 5622 here - and on Windows
that is enough to pull in Boost.Container, which is what now fails. Naming the
target in any form is the problem, so name the include path instead.

Full b2 run, link=shared,static variant=debug,release: zero failures, and b2
no longer references libs/thread or libs/type_erasure at all. CMake:
super-project 91/91, standalone 150/150, building neither.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Its invariant - the v-table pointer matches the type of the value in the
`any` - cannot be maintained: the handle points to an `any` owned by
someone else, who can replace the contained value behind its back. The
handle is then stale, and the next call dispatches to the wrong
overrider and casts to the wrong type, silently.

It buys nothing that is not already available: a method parameter of
type `const virtual_any&` caches the v-table pointer just as well, at no
copy, and with the invariant intact. The only ground the handle covers
on its own is precisely the one where the invariant is unenforceable.

The cost of keeping it was a header, three deduction guides, ==/!=, its
own virtual_traits and the whole set of detail specializations, plus the
defensive machinery guarding the hole: const-only get/*/->, `emplace` as
the sole mutation route, deleted rvalue overloads. And it was crippled
anyway - overriders on the contained value could not use
BOOST_OPENMETHOD_OVERRIDE.

IsVirtualAny stays: it still keeps a `virtual_any` of another
specialization from being stored inside the `any`.

The /permissive- exception in the compile-fail tests went with
compile_fail_virtual_any_from_ptr, so the b2 glob loses its exclusion.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Carries the virtual_any_ptr removal through to this branch's own uses of
it, in the merge itself so that every commit builds:

- boost_type_erasure.hpp: drop the handle from the list of supported
  virtual parameter forms;
- interop_type_erasure.adoc: the `virtual_any` section no longer offers
  the non-owning counterpart;
- test_virtual_any_type_erasure.cpp: drop the virtual_any_ptr test case
  and the include.

Conflicts: doc/modules/ROOT/pages/interop_any.adoc, test/CMakeLists.txt -
both resolved in favor of the removal, keeping this branch's
compile_fail_type_erasure_custom_rtti entry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Remove use_std_any_types and use_boost_any_types. The registration they
performed carried no information the library does not already have: each
type always got the `any` root as its sole base, and every useful type is
statically visible at an instantiation point the library controls. An
inline registrar variable template (detail::use_any_classes) is now
odr-used from the any interops' virtual_traits - cast registers the
overrider's parameter type under the root, vptr registers the root - and
from virtual_any's value constructor, value assignment and emplace, which
register the stored type. All registrars run at static initialization,
like every other registrar, so the types are known before initialize().

A registered type with no specific overrider still falls back on the
catch-all overrider. A type never named statically anywhere remains a
missing_class error at the dynamic vptr lookup - including when
constructing or assigning a virtual_any from an `any` that contains one.

The inline variable has vague linkage; the dlclose exposure is the same
as the existing inplace_vptr_use_classes precedent. Under hidden
visibility, per-module copies are retained by augment_classes' (type,
static_vptr) dedup and all get patched at initialize().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bring in automatic registration of any-contained types. Conflict
resolution keeps detail::use_any_types_aux, which use_type_erasure_types
still builds on, and reworks the const-ref catch-all test blocks to
register `double` via a `weigh` overrider instead of the deleted
use_std_any_types/use_boost_any_types.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extend the any interop's automatic registration to the Boost.TypeErasure
interop, and remove use_type_erasure_types - and use_any_types_aux, its
last support. The virtual_traits' cast registers the overrider's
parameter type as a class derived from the owning any - the root for the
Concept - and vptr registers the root; virtual_any's value operations
already register the stored type generically. openmethod_vptr::apply now
odr-uses the shared use_any_classes registrar instead of its own.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
virtual_any delivered its v-table pointer through a boost_openmethod_vptr
hidden friend - the hook meant for objects that carry their own pointer.
That is the wrong category for a wide type, and it cost:

- the friend returned the pointer by value, where virtual_traits::vptr is
  required to return a reference, so that an indirect registry observes a
  re-initialize;
- has_vptr_fn<virtual_any> was true, so acquire_vptr rejected it with a
  message written for inplace_vptr classes;
- the friend needed a deduced-Self constraint, because MSVC's permissive
  mode injects hidden friends into the enclosing namespace, where a plain
  `any` became a candidate through its implicit conversion to virtual_any,
  and the conversion acquires the pointer, calling the friend again;
- the `any` root class was registered only as a side effect of cast's
  non-catch-all branch, not by dispatch itself.

Replace it with vptr on the three virtual_traits specializations, reading
the cached pointer through a private vptr_ref(). virtual_any now follows
the same convention as every other interop's traits.

Wrapping a virtual_any in a virtual_ptr was previously rejected only as a
side effect of the friend. Reject it explicitly, with a partial
specialization on virtual_ptr's sfinae parameter. That also closes a hole
final_virtual_ptr had all along: it does not go through acquire_vptr, so
it silently returned the static_vptr of the `any` root class rather than
the one for the contained value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The combination was never exercised: the indirect test uses the plain
Concept, which dispatches through typeid_of, and the openmethod_vptr tests
use the default registry.

It needs no support code. `apply` reads `static_vptr` on every call, so it
tracks a re-initialize on its own - unlike inplace_vptr, the other hook
user, which caches the pointer and must store a `const vptr_type*` under
this policy. Assert that the hook still yields the current `static_vptr`
after a second initialize relocates the v-tables, and that dispatch keeps
working. Assert tracking rather than a changed address: the address does
move, but requiring it to would be flaky.

Also pin the registry keying: the hook is found for the registry the
concept names, and for no other. A mismatch is not an error - it falls
back on the vptr policy's hash lookup, silently losing the constant-time
property the concept exists to provide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The concept was documented as making a virtual_any unnecessary: "and does not
need to be: both fill the same goal, constant-time access to the v-table
pointer". Both are constant-time, but they do not cost the same, and the
codegen contradicts "does not need to be". The concept reaches the pointer
through a call on the any's own dispatch table; a virtual_any loads it from
the wrapper. What the concept removes, relative to a plain any, is the hash of
typeid_of, not the call.

Lead instead with the reason the combination is actually rejected - the hook
returns the pointer by value, and an indirect registry cannot store that -
and put the two dispatch sequences side by side on the page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two notes for the agent.

Disassembly goes in Intel syntax, never AT&T.

A table cell holding a block - a code listing, a nested list - must be
introduced with `a|`, not `|`. Without the `a` the cell is not parsed as
AsciiDoc and the [source] / ---- markup renders literally, with no warning
from asciidoctor: the same silent-mis-parse class as the backtick-apostrophe
trap already documented there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
124 lines down to 40. The mechanism is unchanged; what goes is the
file-by-file inventory of test/dynamic_loading/ - derivable from the repo -
and the long MSVC error-code rationale for why registry_state is a separate
thin class, kept now as a single clause.

What stays is everything an agent would otherwise get wrong: that the state
is keyed on Registry::registry_type and never the derived struct, that the
explicit instantiations must not be hand-written because each spelling fails
on exactly one platform, that EXPORT is load-bearing on ELF under hidden
visibility, and that registries are structs rather than aliases on purpose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
boostlook gives a `stretch` table both min-width:100% and margin-left:2rem,
so the table overhangs its section by 2rem; the enclosing .sect3, which
boostlook makes overflow-x:auto, then always shows a horizontal scrollbar.
%autowidth emits fit-content instead, which no boostlook rule targets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
boostlook gives a `stretch` table both min-width:100% and margin-left:2rem, so
the table overhangs its section by 2rem, and the .sect3 around it is
overflow-x:auto - both tables carried a horizontal scrollbar. %autowidth emits
fit-content instead, which no boostlook rule targets.

Also drop the `# TAILCALL` annotations from the disassembly, reword the
self-referential-Concept paragraph in terms of CRTP, and remove the two
comments in type_erasure_concept.cpp that restated the surrounding prose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jll63
jll63 merged commit 65cc955 into boostorg:develop Aug 22, 2026
54 checks passed
@jll63
jll63 deleted the feature/type_erasure branch August 22, 2026 20:22
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