Skip to content

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

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

jll63 wants to merge 3 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.

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

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

bad_call argument types without dynamic_type

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

  • For a polymorphic class, nothing changes.
  • Otherwise, for a virtual_ptr, the class is found from the v-table pointer, by scanning the registry's classes on the error path only. If v-table pointers coincide, only classes deriving from the parameter's class count.
  • If that is still not conclusive, the parameter's class is reported: less precise, but true.

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. A small scanner reads the marks, skipping source names by their length so that identifiers like ZLibStream are not mistaken for them.

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 and local-class detection.
  • test_static_rtti_errors.cpp: no_overrider and ambiguous_call with both policies plus throw_error_handler, checking that the dynamic class is reported.
  • test_std_rtti_type_index.cpp: the scanner on names produced by GCC, Clang and libc++.
  • 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 scanner on Clang + libstdc++ reproduces the collision.
  • Making the v-table lookup return the parameter's class fails the errors test.

Verified locally:

Build Result
CMake, clang-22 + libstdc++ 206/206
CMake, clang-22 + libc++ 201/201, plus the shared-library tests under hidden visibility
C++26 reflection (g++-16) 202/202
b2, gcc-13 (-Wextra -Werror) full suite passes
MSVC v18 the new tests, including a real DLL
dev/check-flat.sh, Antora pass

🤖 Generated with Claude Code

jll63 and others added 3 commits October 6, 2026 18:26
…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>
@cppalliance-bot

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-06 23:20:58 UTC

@codecov

codecov Bot commented Oct 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.60000% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.49%. Comparing base (f093df7) to head (cec7256).
⚠️ Report is 1 commits behind head on develop.

Files with missing lines Patch % Lines
include/boost/openmethod/core.hpp 55.88% 15 Missing ⚠️
include/boost/openmethod/policies/std_rtti.hpp 81.94% 12 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop     #127      +/-   ##
===========================================
- Coverage    92.34%   91.49%   -0.85%     
===========================================
  Files           27       28       +1     
  Lines         2234     2352     +118     
  Branches       679      713      +34     
===========================================
+ Hits          2063     2152      +89     
- Misses         112      138      +26     
- Partials        59       62       +3     
Files with missing lines Coverage Δ
include/boost/openmethod/detail/type_key.hpp 100.00% <100.00%> (ø)
include/boost/openmethod/policies/static_rtti.hpp 100.00% <100.00%> (ø)
include/boost/openmethod/preamble.hpp 78.57% <ø> (ø)
include/boost/openmethod/policies/std_rtti.hpp 83.75% <81.94%> (-16.25%) ⬇️
include/boost/openmethod/core.hpp 87.57% <55.88%> (-4.24%) ⬇️

... 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 108fd89...cec7256. Read the comment docs.

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

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