Skip to content

named_static_rtti; bad_call types without dynamic_type; std_rtti internal-linkage collisions - #127

Open
jll63 wants to merge 10 commits into
boostorg:developfrom
jll63:feature/dll
Open

jll63 wants to merge 10 commits into
boostorg:developfrom
jll63:feature/dll

Conversation

@jll63

@jll63 jll63 commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

(Written by Claude Code, on behalf of @jll63.)

Three fixes around class identity across shared libraries, and within one program, plus one settled contract between the rtti policy and the library, and a rework of the shared-libraries page.

named_static_rtti: a static_rtti that works across shared libraries

static_rtti's type_id is the address of a local static in a template of the library. Each module has its own copy: always on Windows, and under -fvisibility=hidden on ELF and Mach-O. initialize() takes the copies for unrelated classes and methods, and a call made in one module on an object created in another lands in the wrong overrider, or reads past the end of a v-table.

named_static_rtti, in its own header <boost/openmethod/policies/named_static_rtti.hpp>, keeps per-module addresses as type ids, but points them at the name of the class, cut out of __PRETTY_FUNCTION__ / __FUNCSIG__, and its type_index returns that name, so initialize() merges the copies. All modules must be built with the same compiler.

Classes with internal linkage, or none, may print the same name as a different class, so they are keyed on their type_id as well and never merged:

  • With C++26 reflection: has_external_linkage decides, exactly.
  • Without it: the marks the compilers leave in the name decide. Anonymous namespaces, lambdas and unnamed classes are marked by every compiler; local classes only by GCC. Clang prints a local class unqualified and MSVC as if its function were a namespace. The documentation warns about this.

The class-keys MSVC prints are dropped, at the head of a name and inside template arguments, so struct Box<struct Dog,enum Color> becomes Box<Dog,Color>. Two modules that disagree on the key of a class - class Dog; in one against struct Dog in the other, which is only a warning - still agree on its name, and a class prints the same name on every compiler, which is what a diagnostic shows. A compile-time probe of the compiler's own output gates it, so only MSVC pays for the storage a stripped name needs; elsewhere the name stays a view into the signature literal, which the program carries anyway.

static_rtti itself is unchanged: it stays correct within one module, and its documentation now points to named_static_rtti.

bad_call argument types, and what is_polymorphic means

init_bad_call called Rtti::dynamic_type unguarded, so static_rtti or named_static_rtti combined with any error handler did not compile. It now reports:

  • the dynamic type, if the rtti policy deems the class polymorphic;
  • otherwise the class whose v-table the argument uses, through a virtual_ptr or a pointer stored in the object (inplace_vptr), found by scanning the registry's classes on the error path only. Copies of one class from several modules count as one;
  • otherwise the class of the parameter: less precise, but true.

Fixing that exposed a contract nothing stated. is_polymorphic<T> means the policy's dynamic_type can read the class of a T, so a policy that says true owes a dynamic_type, and the library asks for the class of no other class. The first attempt here made dynamic_type optional instead, which contradicted final_virtual_ptr - it calls dynamic_type under runtime checks whenever is_polymorphic holds - and let a test policy claim polymorphism without providing one.

detail::dynamic_type_of is now the one route to rtti::dynamic_type and asserts both halves of the contract, with a readable message instead of "no member named dynamic_type". final_virtual_ptr, init_bad_call and the dynamic_vptr of both vptr policies go through it; std_rtti::dynamic_type asserts for itself. The else return static_type<T>() branch in the custom-RTTI examples and tests existed only for the old unguarded call, and becomes that assert, which demonstrates the rule better than dead code did. custom_rtti.adoc had the omission rule wrong - it said dynamic_type may be omitted whenever boost_openmethod_vptr is provided for every class, which final_virtual_ptr already contradicted.

std_rtti: same-named classes with internal linkage

std_rtti::type_index keyed on type_info::name(). Two classes named Helper in anonymous namespaces in different translation units have the same name: _GLOBAL__N_1 is the same everywhere. initialize() merged them, and the call aborted with "ambiguous". This happened with GCC, Clang, libc++ and MSVC alike, within a single module.

type_index now returns std::type_index again where that is exact: MSVC, and GCC with libstdc++. It cannot be used with libc++, which compares the type_info of hidden-visibility classes by address (the reason for the switch to names in #75), nor with Clang and libstdc++, since Clang does not emit GCC's internal-linkage mark.

There, the key is the mangled name, plus the type_id when the mangling shows internal linkage. Only marks that cannot occur in an identifier are looked for, so no parsing is involved: _GLOBAL__N, GCC's ., and Clang's $_ followed by a digit. A bare $ is not one of them - GCC and Clang accept it in an identifier unless -fno-dollars-in-identifiers is given, so a class named Foo$Impl would have been taken for an unnamed one and never merged across modules. A local class of a static function, and a template argument that is the address of a static variable, are not detected; MSVC's decorated names do not tell those apart either.

The documentation of type_index, in the rtti policy blueprint and in custom_rtti.adoc, now states the contract across modules: equal for the same class, different for different classes, in any module.

shared_libraries.adoc

Reworked for brevity: sharing a registry, implicit and dynamic linking, type ids across modules (with named_static_rtti), indirect v-table pointers and custom registries, with the platform specifics gathered in implementation notes at the end. The page states the ownership rule once, at the top: one module owns the registry state and exports it, the others import it; the library owns it in the implicit-linking example, the program in the dynamic-loading one.

Tests

  • implicit_shared_libraries/named_static_rtti: hidden visibility, a v-table layout that makes a mis-merged call land in the wrong overrider, and a class in an anonymous namespace in each module.
  • test_named_static_rtti.cpp: names, keys, local-class detection, and the class-key strip on names spelled the way MSVC spells them, since no other compiler produces one.
  • test_static_rtti_errors.cpp: no_overrider and ambiguous_call with both policies plus throw_error_handler, checking that the dynamic class is reported, and a case where the class is read from an inplace_vptr. The policy invented to mean "polymorphic without dynamic_type" is gone with the contract it contradicted; plain static_rtti covers the case.
  • test_std_rtti_type_index.cpp: the marks on names produced by GCC, Clang and libc++, including names that only resemble a mark (Global, ZLibStream, UtilityClass, Ut after a substitution, Foo$Impl).
  • implicit_shared_libraries/default_registry: gains an anonymous-namespace class in each module.

Controls run locally:

  • Plain static_rtti in the named test reproduces the wrong-overrider landing.
  • Forcing std::type_index on libc++ with hidden visibility reproduces the macOS failure.
  • Disabling the internal-linkage check on Clang + libstdc++ reproduces the collision.
  • Making the v-table lookup return the parameter's class fails the errors test.
  • Forcing the class-key strip off under MSVC fails the name tests, with struct Dog != Dog.

Verified locally, at the head of the branch:

Build Result
CMake, clang-22 + libstdc++ 206/206
CMake, clang-22 + libc++, hidden visibility 201/201
C++26 reflection (g++-16) 202/202
b2, gcc (-Wextra -Werror) 130 passed, none failed, no warnings
MSVC v18 (CMake + Ninja) 371 targets, 201/201, including the named_static_rtti DLL test
dev/check-flat.sh, Antora pass

🤖 Generated with Claude Code

@cppalliance-bot

cppalliance-bot commented Oct 6, 2026 •

Copy link
Copy Markdown

An automated preview of the documentation is available at https://127.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-10-13 21:37:11 UTC

@codecov

codecov Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.45455% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.36%. Comparing base (f093df7) to head (d55d0d4).
⚠️ Report is 3 commits behind head on develop.

Files with missing lines Patch % Lines
include/boost/openmethod/core.hpp 85.71% 3 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop     #127      +/-   ##
===========================================
+ Coverage    92.34%   92.36%   +0.02%     
===========================================
  Files           27       29       +2     
  Lines         2234     2293      +59     
  Branches       679      697      +18     
===========================================
+ Hits          2063     2118      +55     
- Misses         112      114       +2     
- Partials        59       61       +2     
Files with missing lines Coverage Δ
include/boost/openmethod/detail/type_key.hpp 100.00% <100.00%> (ø)
...de/boost/openmethod/policies/named_static_rtti.hpp 100.00% <100.00%> (ø)
include/boost/openmethod/policies/static_rtti.hpp 100.00% <ø> (ø)
include/boost/openmethod/policies/std_rtti.hpp 100.00% <100.00%> (ø)
include/boost/openmethod/preamble.hpp 78.57% <ø> (ø)
include/boost/openmethod/core.hpp 90.93% <85.71%> (-0.88%) ⬇️

... and 1 file 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 a426f03...d55d0d4. 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 8 commits October 11, 2026 12:27
…ibraries

static_rtti's type_id is the address of a local static in a template of the
library. Each module has its own copy: always on Windows, and under
-fvisibility=hidden on ELF and Mach-O. A class used by several modules then has
one type_id per module, initialize() takes the copies for unrelated classes and
methods, and a call made in one module on an object created in another lands
in the wrong overrider or reads past the end of a v-table.

named_static_rtti keeps per-module addresses as type_ids, but points them at
the name of the class, cut out of __PRETTY_FUNCTION__ / __FUNCSIG__, and
type_index returns that name, so initialize() merges the copies, as std_rtti
does with type_info::name(). All modules must be built with the same compiler.

A class with internal linkage, or none, may print the same name as a different
class, so type_index keys it on its type_id as well, and it is never merged.
With C++26 reflection, has_external_linkage decides exactly. Without it, the
marks the compilers leave in the name decide: anonymous namespaces, lambdas and
unnamed classes are marked by every compiler, local classes only by GCC. Clang
prints a local class unqualified and MSVC as if its function were a namespace,
so two same-named local classes are merged there; the documentation says so.

static_rtti itself is unchanged: it stays correct within one module, local
classes included, and its documentation now points to named_static_rtti for
programs that share a registry across modules.

Tests: test/implicit_shared_libraries/named_static_rtti (hidden visibility,
classes in anonymous namespaces on both sides, a v-table layout that makes a
mis-merged call land in the wrong overrider) and test_named_static_rtti.cpp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
init_bad_call called Rtti::dynamic_type on every argument, unguarded. The
method constructor takes the address of fn_not_implemented and fn_ambiguous,
so a registry combining static_rtti or named_static_rtti - which have no
dynamic_type - with an error handler did not compile.

For a class the rtti policy deems polymorphic, nothing changes. Otherwise, for
a virtual_ptr argument, the class is found from the v-table pointer, by
scanning the registry's classes on the error path only. The object's own class
always matches; if another one does too - v-tables with leading unused slots
are pointed to before their start, and may coincide with another's - only the
classes deriving from the parameter's class count. If that is still not
conclusive, or for any other argument, the parameter's class is reported:
less precise, but true. Classes are compared through type_index, so
named_static_rtti's per-module copies count as one.

Under std_rtti, a non-polymorphic class reached through a virtual_ptr is now
reported as the class of the object rather than that of the parameter.

Test: test_static_rtti_errors.cpp, with static_rtti and named_static_rtti,
each with throw_error_handler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
std_rtti::type_index keyed on type_info::name(). Two classes in anonymous
namespaces in different translation units, both named Helper, have the same
name - _GLOBAL__N_1 is the same in every translation unit - so initialize()
merged them into one class, and a call aborted with "ambiguous". This happened
with GCC, Clang, libc++ and MSVC alike, within a single module.

type_index now returns std::type_index again where that is exact: with MSVC,
whose type_info equality compares decorated names that carry a hash per
translation unit for anonymous namespaces, and with GCC and libstdc++, which
compare names except for the classes that GCC marks as having internal linkage.

It cannot be used with libc++, which compares the type_info objects of classes
with hidden visibility by address - the reason for the switch to names, in
boostorg#75 - nor with Clang and libstdc++, since Clang does not emit GCC's mark. There,
the key is the mangled name, plus the type_id when the mangling shows internal
linkage: an anonymous namespace, a static function or variable in an encoding,
or an unnamed type or lambda. A small scanner reads the marks, skipping source
names by their length so that identifiers like ZLibStream are not mistaken for
them, and skipping the numbers that are not lengths.

The key struct of named_static_rtti moves to detail/type_key.hpp, shared by
both policies.

Tests: test_std_rtti_type_index.cpp checks the scanner on names produced by
GCC, Clang and libc++; the default_registry shared-library test gains a class
in an anonymous namespace in each module, which must keep its own overrider.
Forcing std::type_index on libc++ with hidden visibility reproduces the macOS
failure; disabling the scanner on Clang and libstdc++ reproduces the collision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ontract

std_rtti: the mangled-name scanner lost track of source-name lengths after a
substitution (S_), a template parameter, an array or a vector type, read the
value of a nested enum literal as a length, and treated every L<digit> after a
first Z as a static entity. Identifiers were then read letter by letter: an
`Utility` after `NS_` was taken for the unnamed-type mark, and a
`_GLOBAL__N` after a nested enum literal was skipped, merging distinct
anonymous-namespace classes.

Replace it with substring marks that cannot occur in an identifier:
`_GLOBAL__N`, `$` (Clang's unnamed types and lambdas) and `.` (GCC's). Classes
local to static functions, and templates on the address of a static variable,
are no longer recognized - MSVC's std::type_index does not tell them apart
either - and the documentation of std_rtti::type_index now says so, describes
the key type as unspecified, and states the narrower guarantee.

named_static_rtti: under reflection, non-class types (overrider function
types, for example) fall back to the name marks instead of counting as
shareable.

class_of_vptr: the tie-breaking pass relied on the bases listed in one
use_classes, and could report an unrelated class whose v-table pointer
coincides. A single match is reported; anything else reports the class of the
parameter, which is always true.

Docs: the RttiFn blueprint and custom_rtti.adoc state the real contract of
type_index - equal across modules, distinct for same-named classes in
anonymous namespaces. @tparam Class now matches the template parameter of
static_type in both static_rtti policies. Bodies that name detail:: twice open
with `using namespace detail;`.

Tests: the four misparsed names from the review are regression cases in
test_std_rtti_type_index.cpp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One "Sharing a Registry" section now holds the macro table, the header idiom,
and a single warning about a translation unit that uses neither macro. The
"exactly one INSTANTIATE" rule was stated four times, and the forgotten-macro
warning twice. The platform rationale for the macros moves into Platform
Details, next to the rest of it. The example commentary and the type-id,
alternatives, indirect-vptr, custom-registry and implementation sections are
tightened.

Factual fixes:

* The dynamic-loading program instantiates the state with INSTANTIATE, not
  EXPORT.
* Its header is like, not the same as, the implicit-linking one.
* Only ELF merges a class's type_info across modules; Mach-O does not, as the
  repository's own Jamfile notes.
* Implicit linking: the library owns the state because Windows and macOS
  resolve imported symbols at link time, and an executable owner would need a
  circular link - not merely because it is "natural".
* The tests mark their classes BOOST_SYMBOL_VISIBLE to unify their type ids
  while keeping per-module static vptrs, not to help the hash; the note now
  gives the remedy instead: on ELF, BOOST_SYMBOL_VISIBLE restores a single
  type_info.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
named_static_rtti: the object a type_id points to is now the key itself - the
name, and the key's own address if the class cannot be shared - so
static_rtti_name goes, type_index returns the stored key, and its return type
is deduced, keeping detail::type_key out of the reference. The local-class
check of the name runs once per type at run time instead of at compile time,
where it cost 0.5-2 ms per type per translation unit. The name extraction no
longer computes a quantity that cancels out, and the reflection fallback is
written once.

init_bad_call: use dynamic_type only when the rtti policy deems the class
polymorphic *and* provides it (has_dynamic_type, from
BOOST_OPENMETHOD_DETAIL_HAS_STATIC_FN). Otherwise recover the class from the
v-table pointer, now also for classes that carry their own
(boost_openmethod_vptr). A custom rtti without dynamic_type, used with
inplace_vptr and an error handler, no longer fails to compile.

std_rtti: measure the mangled name once; BOOST_GCC instead of raw compiler
checks; drop the unused override hook of the platform macro.

Tests: test_static_rtti_errors.cpp is one templated test case over both
registries, with the name check folded into the helper (the separate try block
did not fail if nothing was thrown), plus a case for the inplace_vptr policy
above. test_named_static_rtti.cpp: local_classes entirely under its #if, and
the dispatch checks through a lambda.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
<boost/openmethod/policies/named_static_rtti.hpp> holds named_static_rtti and
its name-extraction and linkage helpers. static_rtti.hpp goes back to what it
was, except for the documentation pointing to named_static_rtti.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rrors

inplace_vptr_base's constructor casts `this` to the derived class before the
C++ v-table pointer of a polymorphic class is set, which UBSan rejects. Make
the rtti policy deem every class polymorphic instead, so the case still covers
a policy that has no dynamic_type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jll63 and others added 2 commits October 11, 2026 12:39
The blueprint showed `std::is_polymorphic_v<Class>` as if it were the
required value. Declare it without an initializer, and say what the library
does with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An rtti policy that deems a class polymorphic owes a dynamic_type for it, and
the library reads the class of an object of no other class. The contract was
implicit, and the branch contradicted itself over it: init_bad_call probed for
dynamic_type and fell back when it was missing, while final_virtual_ptr called
it unguarded under runtime checks, so a policy that took the probe at its word
failed to compile there. A test policy claimed polymorphism without providing
dynamic_type, which only that gap made possible.

detail::dynamic_type_of is now the one route to rtti::dynamic_type, and
asserts both halves of the contract. final_virtual_ptr, init_bad_call and the
dynamic_vptr of both vptr policies go through it; std_rtti::dynamic_type
asserts for itself. The dead `else return static_type<T>()` branch in the
custom-RTTI examples and tests existed only for the old unguarded call, and
becomes that assert, which is the better demonstration. The test policy is
gone: plain static_rtti covers the case, since the inplace classes reach
class_of_vptr through their v-table hook.

named_static_rtti drops the class-keys MSVC prints, at the head of a name and
inside template arguments, so two modules that disagree on the key of a class
- `class Dog;` against `struct Dog`, only a warning - still agree on its name,
and a class prints the same name on every compiler. A compile-time probe of
the compiler's own output gates it, so only MSVC pays for the storage a
stripped name needs; elsewhere the name stays a view into the signature.

std_rtti no longer reads a bare `$` as the mark of an unnamed type: GCC and
Clang accept it in an identifier, so a class named Foo$Impl was taken for one
and never merged across modules. Only `$_` followed by a digit counts, which
is Clang's own spelling.

class_of_vptr no longer uses a null type_id as its "not found" sentinel, since
a policy may use a null one as a class's id.

Verified: MSVC v18 (371 targets, 201/201, including the named_static_rtti DLL
test), clang-22 with libstdc++ (206/206) and with libc++ under hidden
visibility (201/201), g++-16 with reflection (202/202), b2 gcc with no
warnings, check-flat.sh, Antora. Forcing the class-key strip off under MSVC
fails the name tests with `struct Dog != Dog`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

2 participants