From d00bfcebc1d54975efba2b03db5b1b4925090ef5 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Tue, 6 Oct 2026 18:26:05 -0400 Subject: [PATCH 1/9] policies: 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. 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 --- doc/modules/ROOT/pages/ref_headers.adoc | 5 +- doc/modules/ROOT/pages/shared_libraries.adoc | 9 + .../boost/openmethod/policies/static_rtti.hpp | 225 ++++++++++++++++++ test/implicit_shared_libraries/CMakeLists.txt | 1 + test/implicit_shared_libraries/Jamfile | 1 + .../named_static_rtti/CMakeLists.txt | 37 +++ .../named_static_rtti/Jamfile | 33 +++ .../named_static_rtti/lib.cpp | 75 ++++++ .../named_static_rtti/lib.hpp | 52 ++++ .../named_static_rtti/main.cpp | 104 ++++++++ .../named_static_rtti/registry.hpp | 28 +++ test/test_named_static_rtti.cpp | 141 +++++++++++ 12 files changed, 709 insertions(+), 2 deletions(-) create mode 100644 test/implicit_shared_libraries/named_static_rtti/CMakeLists.txt create mode 100644 test/implicit_shared_libraries/named_static_rtti/Jamfile create mode 100644 test/implicit_shared_libraries/named_static_rtti/lib.cpp create mode 100644 test/implicit_shared_libraries/named_static_rtti/lib.hpp create mode 100644 test/implicit_shared_libraries/named_static_rtti/main.cpp create mode 100644 test/implicit_shared_libraries/named_static_rtti/registry.hpp create mode 100644 test/test_named_static_rtti.cpp diff --git a/doc/modules/ROOT/pages/ref_headers.adoc b/doc/modules/ROOT/pages/ref_headers.adoc index d07713f5..2a0165e5 100644 --- a/doc/modules/ROOT/pages/ref_headers.adoc +++ b/doc/modules/ROOT/pages/ref_headers.adoc @@ -164,8 +164,9 @@ is defined. That policy needs no header - see above. ### link:{headers-url}/boost/openmethod/policies/static_rtti.hpp[] -Provides a minimal implementation of the `rtti` policy that does not depend on -standard RTTI. +Provides cpp:static_rtti[], a minimal implementation of the `rtti` policy that +does not depend on standard RTTI, and cpp:named_static_rtti[], a variant that +works across shared libraries. ### link:{headers-url}/boost/openmethod/policies/throw_error_handler.hpp[] diff --git a/doc/modules/ROOT/pages/shared_libraries.adoc b/doc/modules/ROOT/pages/shared_libraries.adoc index bd9b26ab..96647512 100644 --- a/doc/modules/ROOT/pages/shared_libraries.adoc +++ b/doc/modules/ROOT/pages/shared_libraries.adoc @@ -272,6 +272,15 @@ in each module; cpp:initialize[] copes - it treats them as several ids for the same class - but they are extra ids for the hash to separate. This is why the library's own shared-library tests mark their classes `BOOST_SYMBOL_VISIBLE`. +NOTE: Under cpp:static_rtti[], a type id is the address of a static variable +in a template of the library, which has hidden visibility under +`-fvisibility=hidden`, and is never shared by a Windows DLL. Each module then +has its own id for every class, and nothing tells cpp:initialize[] that they +designate the same class: it takes them for unrelated classes, and a call made +in one module on an object created in another lands in the wrong overrider. +Use cpp:named_static_rtti[] instead: it maps the ids to the names of the +classes, and cpp:initialize[] merges them. + ### What it costs cpp:fast_perfect_hash[], the default, searches for a multiplier `M` and a shift diff --git a/include/boost/openmethod/policies/static_rtti.hpp b/include/boost/openmethod/policies/static_rtti.hpp index 248bca6a..62a2c884 100644 --- a/include/boost/openmethod/policies/static_rtti.hpp +++ b/include/boost/openmethod/policies/static_rtti.hpp @@ -8,6 +8,13 @@ #include +#include + +#include +#include +#include +#include + namespace boost::openmethod::policies { //! Minimal implementation of the `rtti` policy. @@ -19,6 +26,13 @@ namespace boost::openmethod::policies { //! disabled. `virtual_ptr` must be constructed using @ref final_virtual_ptr (or //! its equivalents for smart pointers). //! +//! Each module of a program has its own copy of those variables, unless the +//! dynamic linker merges them, which it does not do on Windows, nor for symbols +//! with hidden visibility. A class used by several modules then has one +//! `type_id` per module, and the registry takes the copies for unrelated +//! classes. Use @ref named_static_rtti in a program that shares a registry +//! across modules. +//! //! @par Example //! //! Selecting the policy. The registry is declared before @@ -61,4 +75,215 @@ struct static_rtti : rtti { } // namespace boost::openmethod::policies +namespace boost::openmethod::detail { + +// The signature of this function, as the compiler prints it, contains the name +// of `T`. Its prefix and suffix are the same for every `T`; they are measured +// on `void`. +template +constexpr auto static_rtti_signature() -> const char* { + return BOOST_CURRENT_FUNCTION; +} + +inline constexpr std::string_view static_rtti_probe = + static_rtti_signature(); +inline constexpr std::size_t static_rtti_prefix = + static_rtti_probe.find("void"); + +template +constexpr auto static_rtti_type_name() -> std::string_view { + // Without __PRETTY_FUNCTION__ or __FUNCSIG__, BOOST_CURRENT_FUNCTION falls + // back on a name that does not contain `T`, and every class would get the + // same name. + static_assert( + static_rtti_prefix != std::string_view::npos, + "named_static_rtti requires __PRETTY_FUNCTION__ or __FUNCSIG__"); + + constexpr std::size_t suffix = + static_rtti_probe.size() - static_rtti_prefix - 4; + constexpr std::string_view signature = static_rtti_signature(); + + return signature.substr( + static_rtti_prefix, signature.size() - static_rtti_prefix - suffix); +} + +constexpr auto static_rtti_is_qualifier(char c) -> bool { + return (c >= 'a' && c <= 'z') || c == ' ' || c == '&'; +} + +// A class with internal linkage, or none, may print the same name as a +// different class. Only the compilers know which classes they are; this guesses +// it from the marks they leave in the name. GCC prints a local class as +// `f()::L`, or `S::f() const::L`: a scope closed by a parenthesis, maybe +// followed by qualifiers. +constexpr auto static_rtti_marked_local(std::string_view name) -> bool { + constexpr std::string_view marks[] = { + "{anonymous}", // GCC + "(anonymous namespace)", // Clang + "`", // MSVC: `anonymous-namespace', `anonymous namespace' + " 0 && static_rtti_is_qualifier(name[i - 1])) { + --i; + } + + if (i > 0 && name[i - 1] == ')') { + return true; + } + } + + return false; +} + +// Whether the copies of `T`'s type_id in different modules designate the same +// type, and can be merged by name. +template +constexpr auto static_rtti_shareable() -> bool { +#if BOOST_OPENMETHOD_HAS_REFLECTION + if constexpr ( + std::is_class_v || std::is_union_v || std::is_enum_v) { + return std::meta::has_external_linkage(^^T); + } else { + return true; + } +#else + return !static_rtti_marked_local(static_rtti_type_name()); +#endif +} + +// What a named_static_rtti type_id points to. +struct static_rtti_name { + std::string_view name; + bool shareable; +}; + +// What named_static_rtti::type_index returns: the name, and for a class that +// cannot be shared, the type_id as well, which keeps it apart from other +// classes with the same name. +struct static_rtti_key { + std::string_view name; + const void* local; + + friend auto operator==(const static_rtti_key& a, const static_rtti_key& b) + -> bool { + return a.local == b.local && a.name == b.name; + } + + friend auto operator!=(const static_rtti_key& a, const static_rtti_key& b) + -> bool { + return !(a == b); + } + + friend auto operator<(const static_rtti_key& a, const static_rtti_key& b) + -> bool { + if (a.name != b.name) { + return a.name < b.name; + } + + return std::less()(a.local, b.local); + } +}; + +} // namespace boost::openmethod::detail + +template<> +struct std::hash { + auto operator()(const boost::openmethod::detail::static_rtti_key& key) const + -> std::size_t { + return std::hash()(key.name) ^ + std::hash()(key.local); + } +}; + +namespace boost::openmethod::policies { + +//! Implementation of the `rtti` policy that identifies classes by name. +//! +//! `named_static_rtti` is a @ref static_rtti that works across modules. It +//! categorizes all types as non-polymorphic, and its type ids are addresses +//! that differ from one module to the next, like those of @ref static_rtti. In +//! addition, `type_index` maps them to the name of the class, as the compiler +//! prints it, so the registry merges the copies of a class coming from +//! different modules. +//! +//! All the modules must be built with the same compiler, because the names +//! must be spelled the same way. The cost is paid once, by @ref initialize. +//! Dispatch does not use names. +//! +//! A class that has internal linkage, or none, is not shared: it is identified +//! by its `type_id`, as with @ref static_rtti. This covers the classes defined +//! in an anonymous namespace, local classes, lambdas, unnamed classes, and the +//! templates instantiated with any of these. With C++26 reflection, the policy +//! asks the compiler which classes they are. Otherwise, it recognizes them from +//! the marks that the compiler leaves in their names. +//! +//! @warning Clang and MSVC leave no mark in the name of a local class. Clang +//! prints it unqualified, and MSVC prints it as if the function were a +//! namespace (`main::Local`). Without reflection, two local classes that print +//! the same name are taken for the same class, and so is a local class that +//! prints the same name as a class at namespace scope. So is a class template +//! instantiated with an entity that has internal linkage, such as the address +//! of a `static` variable, with all compilers. +//! +//! @see [Custom RTTI](xref:ROOT:custom_rtti.adoc) +struct named_static_rtti : rtti { + //! A RttiFn metafunction. + //! + //! @tparam Registry The registry containing this policy. + template + struct fn : static_rtti::fn { + //! Returns the @ref type_id of `Class`. + //! + //! @tparam Class A class. + template + static auto static_type() -> type_id { + // Not const: two local classes can have the same name, and a + // linker may fold identical read-only objects into one. + static detail::static_rtti_name id{ + detail::static_rtti_type_name(), + detail::static_rtti_shareable() + }; + + return &id; + } + + //! Returns a key that identifies a class across modules. + //! + //! @param type A `type_id`. + //! @return The name of the class, and `type` itself if the class + //! cannot be shared. + static auto type_index(type_id type) -> detail::static_rtti_key { + auto id = static_cast(type); + + return {id->name, id->shareable ? nullptr : type}; + } + + //! Writes the name of a class to a stream. + //! + //! @param type A `type_id`. + //! @param stream A stream to write to. + template + static void type_name(type_id type, Stream& stream) { + stream << static_cast(type)->name; + } + }; +}; + +} // namespace boost::openmethod::policies + #endif diff --git a/test/implicit_shared_libraries/CMakeLists.txt b/test/implicit_shared_libraries/CMakeLists.txt index e6877e56..b338919f 100644 --- a/test/implicit_shared_libraries/CMakeLists.txt +++ b/test/implicit_shared_libraries/CMakeLists.txt @@ -21,3 +21,4 @@ message(STATUS "Boost.OpenMethod: building implicit shared library tests") add_subdirectory(default_registry) add_subdirectory(custom_registry) +add_subdirectory(named_static_rtti) diff --git a/test/implicit_shared_libraries/Jamfile b/test/implicit_shared_libraries/Jamfile index 0f11d53d..4b378971 100644 --- a/test/implicit_shared_libraries/Jamfile +++ b/test/implicit_shared_libraries/Jamfile @@ -42,3 +42,4 @@ project build-project default_registry ; build-project custom_registry ; +build-project named_static_rtti ; diff --git a/test/implicit_shared_libraries/named_static_rtti/CMakeLists.txt b/test/implicit_shared_libraries/named_static_rtti/CMakeLists.txt new file mode 100644 index 00000000..380dd6aa --- /dev/null +++ b/test/implicit_shared_libraries/named_static_rtti/CMakeLists.txt @@ -0,0 +1,37 @@ +# Copyright (c) 2017-2026 Jean-Louis Leroy +# Distributed under the Boost Software License, Version 1.0. +# See accompanying file LICENSE_1_0.txt +# or copy at http://www.boost.org/LICENSE_1_0.txt) + +# named_static_rtti shared across an implicitly linked library: the library and +# the executable have different type_ids for the same class, and the registry +# merges them by name. See main.cpp. + +set(lib boost_openmethod-isl_named_static_rtti_lib) +set(exe boost_openmethod-test_implicit_shared_libraries_named_static_rtti) + +add_library(${lib} SHARED lib.cpp) +target_link_libraries(${lib} PRIVATE Boost::openmethod) + +add_executable(${exe} main.cpp) +target_link_libraries(${exe} + PRIVATE Boost::openmethod Boost::unit_test_framework ${lib}) + +# Hidden visibility is the configuration under test - it is b2's default, and +# what BoostRoot.cmake sets. At default visibility, ELF unifies the static +# variables behind the type_ids across modules, and there is nothing to merge. +# Windows DLLs never share them, whatever the setting. +set_target_properties(${lib} ${exe} PROPERTIES + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON) + +# See custom_registry/CMakeLists.txt. +set_target_properties(${lib} PROPERTIES + LIBRARY_OUTPUT_DIRECTORY $ + RUNTIME_OUTPUT_DIRECTORY $) + +boost_openmethod_add_test(${exe}) + +if (TARGET tests) + add_dependencies(tests ${exe} ${lib}) +endif() diff --git a/test/implicit_shared_libraries/named_static_rtti/Jamfile b/test/implicit_shared_libraries/named_static_rtti/Jamfile new file mode 100644 index 00000000..91de5f77 --- /dev/null +++ b/test/implicit_shared_libraries/named_static_rtti/Jamfile @@ -0,0 +1,33 @@ +# Boost.OpenMethod Library - implicit_shared_libraries/named_static_rtti Jamfile +# +# Copyright 2017-2026 Jean-Louis Leroy +# +# Distributed under the Boost Software License, Version 1.0. +# See accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt + +# named_static_rtti shared across an implicitly linked library: the library and +# the executable have different type_ids for the same class, and the registry +# merges them by name. See main.cpp. +# +# test executable ──linked──▶ lib (owns and exports the registry state) +# +# global is deliberately NOT set: hidden visibility, b2's +# default, is the configuration under test. At global visibility, ELF unifies +# the type_ids across modules, and there is nothing to merge. + +import testing ; + +lib boost_openmethod-isl_named_static_rtti_lib + : lib.cpp + : shared + ; + +run main.cpp + boost_openmethod-isl_named_static_rtti_lib + /boost/test//boost_unit_test_framework/off/static + : + : + : shared + : implicit_shared_libraries_named_static_rtti + ; diff --git a/test/implicit_shared_libraries/named_static_rtti/lib.cpp b/test/implicit_shared_libraries/named_static_rtti/lib.cpp new file mode 100644 index 00000000..6f6648cb --- /dev/null +++ b/test/implicit_shared_libraries/named_static_rtti/lib.cpp @@ -0,0 +1,75 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +// This library owns and exports the registry state; main.cpp imports it, as in +// the custom_registry variant. +#define OWNS_REGISTRY_STATE +#define LIB_SOURCE + +#include "lib.hpp" +#include "../../test_classes.hpp" + +BOOST_OPENMETHOD_INSTANTIATE_REGISTRY(static_registry); + +using namespace boost::openmethod; + +BOOST_OPENMETHOD_TEST_CLASSES(Object, Animal, Dog, Cat); + +namespace { + +// main.cpp has a Stray of its own, and the compiler prints the same name for +// both. They are different classes, and must not be merged. +struct Stray : Animal {}; + +} // namespace + +BOOST_OPENMETHOD_CLASSES(Animal, Stray); + +// A method the executable knows nothing about. Its virtual parameter is a base +// of Animal, so it takes slot 0 in the v-tables of the library's classes, and +// speak takes slot 1. +BOOST_OPENMETHOD(name, (virtual_ptr), const char*); + +BOOST_OPENMETHOD_OVERRIDE(name, (virtual_ptr), const char*) { + return "Object"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (virtual_ptr), const char*) { + return "Dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { + return "?"; +} + +BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { + return "woof"; +} + +BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { + return "library stray"; +} + +auto lib_registry_state_id() -> const void* { + return static_registry::id(); +} + +auto lib_dog_type_id() -> type_id { + return static_registry::rtti::static_type(); +} + +auto lib_speak(virtual_ptr animal) -> const char* { + return speak(animal); +} + +auto lib_make_dog() -> unique_virtual_ptr { + return make_unique_virtual(); +} + +auto lib_make_stray() -> unique_virtual_ptr { + return make_unique_virtual(); +} + +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/implicit_shared_libraries/named_static_rtti/lib.hpp b/test/implicit_shared_libraries/named_static_rtti/lib.hpp new file mode 100644 index 00000000..9d8e9875 --- /dev/null +++ b/test/implicit_shared_libraries/named_static_rtti/lib.hpp @@ -0,0 +1,52 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#ifndef BOOST_OPENMETHOD_TEST_IMPLICIT_SHARED_LIBRARIES_NAMED_STATIC_RTTI_LIB_HPP +#define BOOST_OPENMETHOD_TEST_IMPLICIT_SHARED_LIBRARIES_NAMED_STATIC_RTTI_LIB_HPP + +// Defines static_registry, sets it as the default registry, and exports or +// imports its state depending on OWNS_REGISTRY_STATE. Must come before any +// other openmethod header. +#include "registry.hpp" + +#include + +// named_static_rtti treats every class as non-polymorphic, so the classes need +// not be polymorphic. They are visible, as they would have to be under +// std_rtti, but that does not merge their type_ids: those are static variables +// of the library's own templates, which have hidden visibility. +struct BOOST_SYMBOL_VISIBLE Object {}; + +struct BOOST_SYMBOL_VISIBLE Animal : Object {}; + +struct BOOST_SYMBOL_VISIBLE Dog : Animal {}; + +struct BOOST_SYMBOL_VISIBLE Cat : Animal {}; + +BOOST_OPENMETHOD(speak, (boost::openmethod::virtual_ptr), const char*); + +#if defined(LIB_SOURCE) +#define LIB_API BOOST_SYMBOL_EXPORT +#else +#define LIB_API BOOST_SYMBOL_IMPORT +#endif + +// The address identifying the registry's shared state, as seen from inside the +// library. +LIB_API auto lib_registry_state_id() -> const void*; + +// static_type(), as the library computes it. +LIB_API auto lib_dog_type_id() -> boost::openmethod::type_id; + +// Dispatch performed inside the library, on an object created in either module. +LIB_API auto lib_speak(boost::openmethod::virtual_ptr animal) -> const + char*; + +LIB_API auto lib_make_dog() -> boost::openmethod::unique_virtual_ptr; + +// An instance of the library's own Stray, a class in an anonymous namespace. +LIB_API auto lib_make_stray() -> boost::openmethod::unique_virtual_ptr; + +#endif diff --git a/test/implicit_shared_libraries/named_static_rtti/main.cpp b/test/implicit_shared_libraries/named_static_rtti/main.cpp new file mode 100644 index 00000000..9a6c96dd --- /dev/null +++ b/test/implicit_shared_libraries/named_static_rtti/main.cpp @@ -0,0 +1,104 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +// named_static_rtti across an implicitly linked shared library built with +// hidden visibility - b2's default, and what BoostRoot.cmake sets - or across a +// Windows DLL. +// +// Like static_rtti, named_static_rtti's type_ids are addresses of static +// variables, and each module has its own copy: the library and the executable +// have different type_ids for the same class. With static_rtti, initialize() +// takes them for unrelated classes, and lays out each module's v-tables on its +// own: a call made in one module, on an object created in the other, lands in +// the wrong overrider, or reads past the end of a v-table. named_static_rtti +// maps the type_ids to the names of the classes, and initialize() merges them. + +#define BOOST_TEST_MODULE implicit_shared_libraries_named_static_rtti + +#include + +#include "lib.hpp" +#include "../../test_classes.hpp" + +#include + +#include + +using namespace boost::openmethod; + +BOOST_OPENMETHOD_TEST_CLASSES(Object, Animal, Dog, Cat); + +namespace { + +// Not the library's Stray, although the compiler prints the same name for both. +// This is the collision that every compiler leaves a mark for; local classes +// are marked only by GCC. +struct Stray : Animal {}; + +} // namespace + +BOOST_OPENMETHOD_CLASSES(Animal, Stray); + +// Registered by the executable; the library knows nothing about it. +BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { + return "meow"; +} + +BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { + return "executable stray"; +} + +struct initialize_fixture { + initialize_fixture() { + initialize(); + } +}; + +BOOST_TEST_GLOBAL_FIXTURE(initialize_fixture); + +BOOST_AUTO_TEST_CASE(one_class_across_modules) { + BOOST_TEST(lib_registry_state_id() == static_registry::id()); + + // The type_ids differ, which is what named_static_rtti copes with... + auto exe_dog_type_id = static_registry::rtti::static_type(); + BOOST_TEST(lib_dog_type_id() != exe_dog_type_id); + + // ...by mapping them to the same key. + BOOST_TEST( + (static_registry::rtti::type_index(lib_dog_type_id()) == + static_registry::rtti::type_index(exe_dog_type_id))); + + // One class, one v-table, whichever module created the object. + auto lib_dog = lib_make_dog(); + auto exe_dog = make_unique_virtual(); + BOOST_TEST(lib_dog.vptr() == exe_dog.vptr()); +} + +BOOST_AUTO_TEST_CASE(executable_dispatches_on_library_object) { + // The library's speak takes slot 1, behind name, which is declared on a + // base class, and the library lays out the v-tables. If the executable + // had a speak of its own, it would take slot 0, and land in name. + BOOST_TEST(std::string(speak(lib_make_dog())) == "woof"); +} + +BOOST_AUTO_TEST_CASE(library_dispatches_on_executable_object) { + BOOST_TEST(std::string(lib_speak(make_unique_virtual())) == "woof"); + BOOST_TEST(std::string(lib_speak(make_unique_virtual())) == "meow"); + BOOST_TEST(std::string(speak(make_unique_virtual())) == "meow"); + BOOST_TEST(std::string(lib_speak(make_unique_virtual())) == "?"); +} + +BOOST_AUTO_TEST_CASE(classes_in_anonymous_namespaces_stay_apart) { + auto lib_stray = lib_make_stray(); + auto exe_stray = make_unique_virtual(); + + BOOST_TEST(lib_stray.vptr() != exe_stray.vptr()); + BOOST_TEST(std::string(speak(lib_stray)) == "library stray"); + BOOST_TEST(std::string(lib_speak(lib_stray)) == "library stray"); + BOOST_TEST(std::string(speak(exe_stray)) == "executable stray"); + BOOST_TEST(std::string(lib_speak(exe_stray)) == "executable stray"); +} + +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/implicit_shared_libraries/named_static_rtti/registry.hpp b/test/implicit_shared_libraries/named_static_rtti/registry.hpp new file mode 100644 index 00000000..673b3ac5 --- /dev/null +++ b/test/implicit_shared_libraries/named_static_rtti/registry.hpp @@ -0,0 +1,28 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#ifndef BOOST_OPENMETHOD_TEST_IMPLICIT_SHARED_LIBRARIES_NAMED_STATIC_RTTI_REGISTRY_HPP +#define BOOST_OPENMETHOD_TEST_IMPLICIT_SHARED_LIBRARIES_NAMED_STATIC_RTTI_REGISTRY_HPP + +#include + +struct static_registry; +#define BOOST_OPENMETHOD_DEFAULT_REGISTRY static_registry + +#include +#include + +struct BOOST_SYMBOL_VISIBLE static_registry : + boost::openmethod::registry< + boost::openmethod::policies::named_static_rtti> {}; + +// The registry state is shared exactly as in the custom_registry variant. +#if defined(OWNS_REGISTRY_STATE) +BOOST_OPENMETHOD_EXPORT_REGISTRY(static_registry); +#else +BOOST_OPENMETHOD_IMPORT_REGISTRY(static_registry); +#endif + +#endif diff --git a/test/test_named_static_rtti.cpp b/test/test_named_static_rtti.cpp new file mode 100644 index 00000000..0ff3888a --- /dev/null +++ b/test/test_named_static_rtti.cpp @@ -0,0 +1,141 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +struct named_registry; +#define BOOST_OPENMETHOD_DEFAULT_REGISTRY named_registry + +#include +#include +#include +#include + +struct named_registry : + boost::openmethod::registry< + boost::openmethod::policies::named_static_rtti> {}; + +#define BOOST_TEST_MODULE openmethod +#include + +#include "test_classes.hpp" + +#include +#include +#include + +struct Animal {}; + +struct Dog : Animal {}; + +struct Cat : Animal {}; + +namespace { + +struct Stray : Animal {}; + +} // namespace + +using namespace boost::openmethod::aliases; + +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat); + +BOOST_OPENMETHOD_CLASSES(Animal, Stray); + +BOOST_OPENMETHOD(poke, (virtual_ptr, std::ostream&), void); + +BOOST_OPENMETHOD_OVERRIDE(poke, (virtual_ptr, std::ostream& os), void) { + os << "bark"; +} + +BOOST_OPENMETHOD_OVERRIDE(poke, (virtual_ptr, std::ostream& os), void) { + os << "hiss"; +} + +BOOST_OPENMETHOD_OVERRIDE(poke, (virtual_ptr, std::ostream& os), void) { + os << "growl"; +} + +using rtti = named_registry::rtti; + +template +auto key() { + return rtti::type_index(rtti::static_type()); +} + +template +auto name() -> std::string { + std::ostringstream os; + rtti::type_name(rtti::static_type(), os); + + return os.str(); +} + +auto ends_with(std::string_view s, std::string_view suffix) -> bool { + return s.size() >= suffix.size() && + s.substr(s.size() - suffix.size()) == suffix; +} + +BOOST_AUTO_TEST_CASE(names) { + // MSVC prints `struct Dog`. + BOOST_TEST(ends_with(name(), "Dog")); + BOOST_TEST(ends_with(name(), "Cat")); +} + +BOOST_AUTO_TEST_CASE(keys) { + BOOST_TEST((key() == key())); + BOOST_TEST((key() != key())); + BOOST_TEST((key() < key() || key() < key())); + + // A class at namespace scope is identified by its name alone... + BOOST_TEST(key().local == nullptr); + + // ...one in an anonymous namespace by its type_id as well, on every + // compiler. + BOOST_TEST(key().local == rtti::static_type()); +} + +BOOST_AUTO_TEST_CASE(local_classes) { + struct Local {}; + auto first = key(); + + { + struct Local {}; + auto second = key(); + +#if BOOST_OPENMETHOD_HAS_REFLECTION || \ + (defined(__GNUC__) && !defined(__clang__)) + // GCC prints both as `local_classes::test_method()::Local`, and marks + // them as local; reflection does not need marks. + BOOST_TEST(first.local != nullptr); + BOOST_TEST((first != second)); +#else + // Clang and MSVC leave no mark; see the warning in the documentation + // of named_static_rtti. + (void)first; + (void)second; +#endif + } +} + +BOOST_AUTO_TEST_CASE(dispatch) { + boost::openmethod::initialize(); + + { + std::stringstream os; + poke(make_unique_virtual(), os); + BOOST_TEST(os.str() == "bark"); + } + { + std::stringstream os; + poke(make_unique_virtual(), os); + BOOST_TEST(os.str() == "hiss"); + } + { + std::stringstream os; + poke(make_unique_virtual(), os); + BOOST_TEST(os.str() == "growl"); + } +} + +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); From 645a1f972503f6e2db73a2a46623f16fbdac7dba Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Tue, 6 Oct 2026 18:39:11 -0400 Subject: [PATCH 2/9] core: report bad_call argument types without dynamic_type 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 --- include/boost/openmethod/core.hpp | 93 +++++++++++++++-- include/boost/openmethod/preamble.hpp | 5 + test/test_static_rtti_errors.cpp | 137 ++++++++++++++++++++++++++ 3 files changed, 226 insertions(+), 9 deletions(-) create mode 100644 test/test_static_rtti_errors.cpp diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index f3a7c474..0353fe03 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -2256,34 +2256,109 @@ using overrider_virtual_types = boost::mp11::mp_remove< MethodParameters, OverriderParameters>, void>; -template +// Whether a registration of a class says that it is `declared`, or derives from +// it. Only the bases listed in the same `use_classes` are known. +template +auto is_or_derives_from(const class_info& ci, type_id declared) -> bool { + using rtti = typename Registry::rtti; + + auto key = rtti::type_index(declared); + + if (rtti::type_index(ci.type) == key) { + return true; + } + + for (auto base = ci.first_base; base != ci.last_base; ++base) { + if (rtti::type_index(*base) == key) { + return true; + } + } + + return false; +} + +// The class of an object, found from its v-table pointer, for an rtti policy +// that cannot tell the dynamic type. Used only to report an error. +// +// The object's own class always matches, because the v-table pointer came from +// its `static_vptr`. Another class may match as well: a v-table whose leading +// slots are unused is pointed to before its start, possibly at the start of +// another class' v-table. Then only a class that derives from `declared` will +// do. If that still does not settle it, `declared` is the answer: it is less +// precise, but true. +template +auto class_of_vptr(vptr_type vptr, type_id declared) -> type_id { + using rtti = typename Registry::rtti; + + if (!vptr) { + return declared; + } + + for (bool derived_only : {false, true}) { + type_id found = nullptr; + bool unique = true; + + for (auto& ci : Registry::state().classes) { + if (*ci.static_vptr != vptr || + (derived_only && !is_or_derives_from(ci, declared))) { + continue; + } + + if (!found) { + found = ci.type; + } else if (rtti::type_index(ci.type) != rtti::type_index(found)) { + unique = false; + break; + } + } + + if (found && unique) { + return found; + } + } + + return declared; +} + +template struct init_bad_call { template static auto fn(bad_call& error, const Arg& arg, const Args&... args) { + using rtti = typename Registry::rtti; + if constexpr (Index == 0u) { - error.method = Rtti::template static_type(); + error.method = rtti::template static_type(); error.arity = sizeof...(args) + 1; } type_id arg_type_id; if constexpr (is_virtual_ptr) { - arg_type_id = Rtti::dynamic_type(*arg); + using Class = std::remove_cv_t; + + if constexpr (rtti::template is_polymorphic) { + arg_type_id = rtti::dynamic_type(*arg); + } else { + arg_type_id = class_of_vptr( + arg.vptr(), rtti::template static_type()); + } + } else if constexpr (rtti::template is_polymorphic) { + arg_type_id = rtti::dynamic_type(arg); } else { - arg_type_id = Rtti::dynamic_type(arg); + arg_type_id = rtti::template static_type(); } error.types[Index] = arg_type_id; - init_bad_call::fn(error, args...); + init_bad_call::fn(error, args...); } static auto fn(bad_call&) { } }; -template -struct init_bad_call { +template +struct init_bad_call { static auto fn(bad_call&) { } }; @@ -3051,7 +3126,7 @@ method::fn_not_implemented( if constexpr (Registry::has_error_handler) { no_overrider error; - detail::init_bad_call::fn( + detail::init_bad_call::fn( error, detail::parameter_traits::peek(args)...); Registry::error_handler::error(error); @@ -3069,7 +3144,7 @@ method::fn_ambiguous( if constexpr (Registry::has_error_handler) { ambiguous_call error; - detail::init_bad_call::fn( + detail::init_bad_call::fn( error, detail::parameter_traits::peek(args)...); Registry::error_handler::error(error); diff --git a/include/boost/openmethod/preamble.hpp b/include/boost/openmethod/preamble.hpp index 8528ee82..b234000a 100644 --- a/include/boost/openmethod/preamble.hpp +++ b/include/boost/openmethod/preamble.hpp @@ -274,6 +274,11 @@ struct bad_call : openmethod_error { //! The maximum size of `types` static constexpr std::size_t max_types = 16; //! The type_ids of the arguments. + //! + //! For a class that the `rtti` policy deems polymorphic, the dynamic type + //! of the argument, as reported by the policy. Otherwise, for a + //! `virtual_ptr`, the class whose v-table it points to, or, if that is not + //! conclusive, the class of the parameter. type_id types[max_types]; }; diff --git a/test/test_static_rtti_errors.cpp b/test/test_static_rtti_errors.cpp new file mode 100644 index 00000000..089f9f05 --- /dev/null +++ b/test/test_static_rtti_errors.cpp @@ -0,0 +1,137 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +// static_rtti and named_static_rtti have no dynamic_type. A bad_call still +// reports the class of each argument: the one whose v-table the virtual_ptr +// points to. + +#include +#include +#include +#include +#include + +#define BOOST_TEST_MODULE openmethod +#include + +#include +#include + +using namespace boost::openmethod; + +struct static_registry : + registry {}; + +struct named_registry : + registry {}; + +struct Animal {}; + +struct Dog : Animal {}; + +struct Cat : Animal {}; + +namespace with_static_rtti { + +using R = static_registry; + +BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat, R); + +BOOST_OPENMETHOD(speak, (virtual_ptr), void); + +BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), void) { +} + +BOOST_OPENMETHOD(meet, (virtual_ptr, virtual_ptr), void); + +BOOST_OPENMETHOD_OVERRIDE( + meet, (virtual_ptr, virtual_ptr), void) { +} + +BOOST_OPENMETHOD_OVERRIDE( + meet, (virtual_ptr, virtual_ptr), void) { +} + +} // namespace with_static_rtti + +namespace with_named_static_rtti { + +using R = named_registry; + +BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat, R); + +BOOST_OPENMETHOD(speak, (virtual_ptr), void); + +BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), void) { +} + +BOOST_OPENMETHOD(meet, (virtual_ptr, virtual_ptr), void); + +BOOST_OPENMETHOD_OVERRIDE( + meet, (virtual_ptr, virtual_ptr), void) { +} + +BOOST_OPENMETHOD_OVERRIDE( + meet, (virtual_ptr, virtual_ptr), void) { +} + +} // namespace with_named_static_rtti + +// Checks that `call` reports `Error`, and that every argument is reported as a +// Dog - the class of the object, not Animal, the class of the parameter. +template +void check_error(Call call, std::size_t arity) { + using rtti = typename Registry::rtti; + + try { + call(); + BOOST_FAIL("should have thrown"); + } catch (const Error& error) { + BOOST_TEST(error.arity == arity); + + for (std::size_t i = 0; i < arity; ++i) { + BOOST_TEST( + (rtti::type_index(error.types[i]) == + rtti::type_index(rtti::template static_type()))); + } + } catch (...) { + BOOST_FAIL("wrong exception"); + } +} + +BOOST_AUTO_TEST_CASE(static_rtti_errors) { + using namespace with_static_rtti; + + initialize(); + Dog dog; + + check_error([&] { speak(final_virtual_ptr(dog)); }, 1); + check_error( + [&] { speak(make_unique_virtual()); }, 1); + check_error( + [&] { meet(final_virtual_ptr(dog), final_virtual_ptr(dog)); }, 2); +} + +BOOST_AUTO_TEST_CASE(named_static_rtti_errors) { + using namespace with_named_static_rtti; + + initialize(); + Dog dog; + + check_error([&] { speak(final_virtual_ptr(dog)); }, 1); + check_error( + [&] { speak(make_unique_virtual()); }, 1); + check_error( + [&] { meet(final_virtual_ptr(dog), final_virtual_ptr(dog)); }, 2); + + try { + speak(final_virtual_ptr(dog)); + } catch (const no_overrider& error) { + std::ostringstream os; + R::rtti::type_name(error.types[0], os); + auto name = os.str(); + BOOST_TEST(name.substr(name.size() - 3) == "Dog"); + } +} From 9416f4cc7a1e172cb3bb06b2b817cefee7475b6e Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Tue, 6 Oct 2026 19:04:42 -0400 Subject: [PATCH 3/9] policies: std_rtti tells apart same-named classes with internal linkage 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 #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 --- include/boost/openmethod/detail/type_key.hpp | 51 +++++ .../boost/openmethod/policies/static_rtti.hpp | 40 +--- .../boost/openmethod/policies/std_rtti.hpp | 211 ++++++++++++++++-- .../default_registry/lib.cpp | 18 ++ .../default_registry/lib.hpp | 3 + .../default_registry/main.cpp | 32 ++- test/test_std_rtti_type_index.cpp | 94 ++++++++ 7 files changed, 390 insertions(+), 59 deletions(-) create mode 100644 include/boost/openmethod/detail/type_key.hpp create mode 100644 test/test_std_rtti_type_index.cpp diff --git a/include/boost/openmethod/detail/type_key.hpp b/include/boost/openmethod/detail/type_key.hpp new file mode 100644 index 00000000..4627accd --- /dev/null +++ b/include/boost/openmethod/detail/type_key.hpp @@ -0,0 +1,51 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#ifndef BOOST_OPENMETHOD_DETAIL_TYPE_KEY_HPP +#define BOOST_OPENMETHOD_DETAIL_TYPE_KEY_HPP + +#include +#include +#include + +namespace boost::openmethod::detail { + +// A key that identifies a class across modules, for an rtti policy that +// identifies classes by name: the name, and for a class that cannot be shared +// across translation units - one with internal linkage, or none - the type_id +// as well, which keeps it apart from other classes with the same name. +struct type_key { + std::string_view name; + const void* local; + + friend auto operator==(const type_key& a, const type_key& b) -> bool { + return a.local == b.local && a.name == b.name; + } + + friend auto operator!=(const type_key& a, const type_key& b) -> bool { + return !(a == b); + } + + friend auto operator<(const type_key& a, const type_key& b) -> bool { + if (a.name != b.name) { + return a.name < b.name; + } + + return std::less()(a.local, b.local); + } +}; + +} // namespace boost::openmethod::detail + +template<> +struct std::hash { + auto operator()(const boost::openmethod::detail::type_key& key) const + -> std::size_t { + return std::hash()(key.name) ^ + std::hash()(key.local); + } +}; + +#endif diff --git a/include/boost/openmethod/policies/static_rtti.hpp b/include/boost/openmethod/policies/static_rtti.hpp index 62a2c884..01456f60 100644 --- a/include/boost/openmethod/policies/static_rtti.hpp +++ b/include/boost/openmethod/policies/static_rtti.hpp @@ -7,11 +7,11 @@ #define BOOST_OPENMETHOD_POLICY_MINIMAL_RTTI_HPP #include +#include #include #include -#include #include #include @@ -172,44 +172,8 @@ struct static_rtti_name { bool shareable; }; -// What named_static_rtti::type_index returns: the name, and for a class that -// cannot be shared, the type_id as well, which keeps it apart from other -// classes with the same name. -struct static_rtti_key { - std::string_view name; - const void* local; - - friend auto operator==(const static_rtti_key& a, const static_rtti_key& b) - -> bool { - return a.local == b.local && a.name == b.name; - } - - friend auto operator!=(const static_rtti_key& a, const static_rtti_key& b) - -> bool { - return !(a == b); - } - - friend auto operator<(const static_rtti_key& a, const static_rtti_key& b) - -> bool { - if (a.name != b.name) { - return a.name < b.name; - } - - return std::less()(a.local, b.local); - } -}; - } // namespace boost::openmethod::detail -template<> -struct std::hash { - auto operator()(const boost::openmethod::detail::static_rtti_key& key) const - -> std::size_t { - return std::hash()(key.name) ^ - std::hash()(key.local); - } -}; - namespace boost::openmethod::policies { //! Implementation of the `rtti` policy that identifies classes by name. @@ -267,7 +231,7 @@ struct named_static_rtti : rtti { //! @param type A `type_id`. //! @return The name of the class, and `type` itself if the class //! cannot be shared. - static auto type_index(type_id type) -> detail::static_rtti_key { + static auto type_index(type_id type) -> detail::type_key { auto id = static_cast(type); return {id->name, id->shareable ? nullptr : type}; diff --git a/include/boost/openmethod/policies/std_rtti.hpp b/include/boost/openmethod/policies/std_rtti.hpp index 8acd6d89..245b535b 100644 --- a/include/boost/openmethod/policies/std_rtti.hpp +++ b/include/boost/openmethod/policies/std_rtti.hpp @@ -7,13 +7,175 @@ #define BOOST_OPENMETHOD_POLICY_STD_RTTI_HPP #include +#include -#ifndef BOOST_NO_RTTI +#include #include + +#ifndef BOOST_NO_RTTI +#include #include #include #endif +// Whether std::type_index identifies classes the way initialize() needs: equal +// for the copies of one class in different modules, and different for two +// classes with internal linkage that have the same name. See +// std_rtti::type_index. +#ifndef BOOST_OPENMETHOD_DETAIL_STD_TYPE_INDEX +#if defined(_MSC_VER) || \ + (defined(__GLIBCXX__) && defined(__GNUC__) && !defined(__clang__) && \ + !defined(__INTEL_COMPILER)) +#define BOOST_OPENMETHOD_DETAIL_STD_TYPE_INDEX 1 +#else +#define BOOST_OPENMETHOD_DETAIL_STD_TYPE_INDEX 0 +#endif +#endif + +namespace boost::openmethod::detail { + +inline auto mangled_is_digit(char c) -> bool { + return c >= '0' && c <= '9'; +} + +// Whether a type name mangled per the Itanium C++ ABI designates a type that a +// different type, in another translation unit, may share the name of: one with +// internal linkage, or none. The mangling says so: +// +// _GLOBAL__N... begins the name of an anonymous namespace +// L at the start of a name, or in the encoding of a function +// or variable (after `Z`, or `_Z` in an expression), names a +// static one: a type local to a static function, a lambda +// in the initializer of a static variable, or a template +// argument that is the address of a static variable +// Ut an unnamed type +// $ or . in a name, Clang's or GCC's name for an unnamed type, or a +// lambda +// +// Source names are skipped by their length, so an identifier like `ZLib` is +// not taken for a mark, and so are the numbers that are not lengths. A mark +// read where there is none only keeps the copies of a class apart, as if it +// had internal linkage. +inline auto itanium_internal_linkage(std::string_view name) -> bool { + const std::size_t n = name.size(); + std::size_t i = 0; + bool encoding = false; + bool literal_value = false; + + auto skip_number = [&]() { + if (i < n && name[i] == 'n') { + ++i; + } + + while (i < n && mangled_is_digit(name[i])) { + ++i; + } + }; + + while (i < n) { + char c = name[i]; + char next = i + 1 < n ? name[i + 1] : '\0'; + + if (mangled_is_digit(c)) { + std::size_t length = 0; + + while (i < n && mangled_is_digit(name[i])) { + length = 10 * length + std::size_t(name[i++] - '0'); + } + + auto identifier = name.substr(i, length); + + // An anonymous namespace, or a name that is not an identifier: + // `$_0` (Clang) or `._anon_0` (GCC) for an unnamed type. + if (identifier.substr(0, 10) == "_GLOBAL__N" || + identifier.find_first_of("$.") != std::string_view::npos) { + return true; + } + + i += length; + + // The value of an enum literal follows its type. + if (literal_value) { + literal_value = false; + skip_number(); + } + + continue; + } + + switch (c) { + case '$': + return true; + + case 'U': + if (next == 't') { + return true; + } + + break; + + case 'Z': + encoding = true; + break; + + case 'L': + if (mangled_is_digit(next)) { + // A static function or variable, in an encoding or at the + // start of a name; an enum literal never comes there. + if (encoding || i == 0 || name[i - 1] == 'N') { + return true; + } + + // An enum literal: L E. + literal_value = true; + } else if (next >= 'a' && next <= 'z') { + // A literal of a builtin type: L E. + i += 2; + skip_number(); + continue; + } + + break; + + case 'S': + // A substitution, S _, or an abbreviation like St. + ++i; + + while ( + i < n && + (mangled_is_digit(name[i]) || + (name[i] >= 'A' && name[i] <= 'Z'))) { + ++i; + } + + continue; + + case 'D': + // A vector type: Dv _. + if (next == 'v') { + i += 2; + skip_number(); + continue; + } + + break; + + case 'T': // a template parameter: T _ + case 'A': // an array type: A _ + case '_': // a discriminator: _ , or __ _ + ++i; + skip_number(); + continue; + } + + ++i; + } + + return false; +} + +} // namespace boost::openmethod::detail + namespace boost::openmethod::policies { //! Implements the @ref rtti policy using standard RTTI. @@ -79,33 +241,44 @@ struct std_rtti : rtti { reinterpret_cast(type)->name()); } - //! Returns a key that uniquely identifies a class. + //! Returns a key that identifies a class across modules. //! //! C++ does *not* guarantee that there is a single instance of //! `std::type_info` per type: a class used by several modules of a //! program typically has one per module. `type_index` maps a `type_id` //! to a key that compares equal for all the type ids of one class, //! which is what @ref initialize uses to group the registrations coming - //! from different modules. + //! from different modules. The key must also tell apart two classes + //! that have the same name, which classes with internal linkage in + //! different translation units can. + //! + //! With MSVC, and with GCC and libstdc++, the key is a + //! `std::type_index`. Their `std::type_info` equality compares names, + //! except for classes with internal linkage, which MSVC names + //! uniquely, and which GCC marks for libstdc++ to compare by address. //! - //! The key is the *name*, not the address and not a `std::type_index`. - //! `std::type_index` would delegate to `std::type_info::operator==`, - //! and that is only as good as the platform's RTTI uniqueness: - //! libstdc++ falls back to comparing names, but libc++ on Darwin - //! compares uniquely-named RTTI by address. There, two modules' - //! `type_info` objects for the same type compare *unequal* unless the - //! symbol happens to be exported and coalesced by dyld - which, for a - //! template like `method`, requires every - //! template argument to have default visibility as well. Under - //! `-fvisibility=hidden` that is not the case, the copies are not - //! grouped, each module's method keeps its own overrider list, and - //! calls report @ref no_overrider. Comparing names sidesteps the - //! platform's uniqueness rules entirely. + //! Elsewhere, `std::type_index` does not do. libc++ compares the + //! `std::type_info` objects of a class with hidden visibility by + //! address, so the copies in different modules are unequal, and Clang + //! does not mark the classes with internal linkage for libstdc++. The + //! key is then the mangled name of the class, plus its `type_id` if + //! the mangled name shows that the class has internal linkage, or + //! none. //! //! @param type A `type_id`. - //! @return The mangled name of the class identified by `type`. - static auto type_index(type_id type) -> std::string_view { - return reinterpret_cast(type)->name(); + //! @return A key identifying the class of `type`. + static auto type_index(type_id type) { + const auto& info = *reinterpret_cast(type); + +#if BOOST_OPENMETHOD_DETAIL_STD_TYPE_INDEX + return std::type_index(info); +#else + const char* name = info.name(); + + return detail::type_key{ + name, detail::itanium_internal_linkage(name) ? type : nullptr + }; +#endif } //! Casts an object to a type. diff --git a/test/implicit_shared_libraries/default_registry/lib.cpp b/test/implicit_shared_libraries/default_registry/lib.cpp index dc331510..f584ad7b 100644 --- a/test/implicit_shared_libraries/default_registry/lib.cpp +++ b/test/implicit_shared_libraries/default_registry/lib.cpp @@ -22,6 +22,16 @@ BOOST_OPENMETHOD_INSTANTIATE_REGISTRY(boost::openmethod::default_registry); BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat); +namespace { + +// main.cpp has a Stray of its own, with the same name. They are different +// classes, and must not be merged. +struct Stray : Animal {}; + +} // namespace + +BOOST_OPENMETHOD_CLASSES(Animal, Stray); + BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { return "?"; } @@ -30,6 +40,10 @@ BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { return "woof"; } +BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { + return "library stray"; +} + auto lib_speak_method_type_id() -> const void* { return &typeid( BOOST_OPENMETHOD_TYPE(speak, (virtual_ptr), const char*)); @@ -47,4 +61,8 @@ auto lib_make_dog() -> unique_virtual_ptr { return make_unique_virtual(); } +auto lib_make_stray() -> unique_virtual_ptr { + return make_unique_virtual(); +} + BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/implicit_shared_libraries/default_registry/lib.hpp b/test/implicit_shared_libraries/default_registry/lib.hpp index d746a9d0..d131820b 100644 --- a/test/implicit_shared_libraries/default_registry/lib.hpp +++ b/test/implicit_shared_libraries/default_registry/lib.hpp @@ -59,4 +59,7 @@ LIB_API auto lib_speak(boost::openmethod::virtual_ptr animal) -> const LIB_API auto lib_make_dog() -> boost::openmethod::unique_virtual_ptr; +// An instance of the library's own Stray, a class in an anonymous namespace. +LIB_API auto lib_make_stray() -> boost::openmethod::unique_virtual_ptr; + #endif diff --git a/test/implicit_shared_libraries/default_registry/main.cpp b/test/implicit_shared_libraries/default_registry/main.cpp index c7a8d2e8..38b34cbb 100644 --- a/test/implicit_shared_libraries/default_registry/main.cpp +++ b/test/implicit_shared_libraries/default_registry/main.cpp @@ -20,11 +20,24 @@ using namespace boost::openmethod; BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat); +namespace { + +// Not the library's Stray, although both have the same name. +struct Stray : Animal {}; + +} // namespace + +BOOST_OPENMETHOD_CLASSES(Animal, Stray); + // Registered by the executable; the library knows nothing about it. BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { return "meow"; } +BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { + return "executable stray"; +} + // The library is linked implicitly, so its static constructors have already run // by the time we get here: a single initialize() sets up dispatch tables that // include both modules' contributions. @@ -52,8 +65,9 @@ BOOST_AUTO_TEST_CASE(shared_registry_state) { << (std::type_index(exe_ti) == std::type_index(lib_ti))); // The invariant that matters is the *policy* key: that is what // augment_methods() groups by. std::type_index deliberately is NOT - // asserted - it is false on Darwin, which is the whole reason - // std_rtti::type_index compares names instead. + // asserted - libc++ compares the type_info of a class with hidden + // visibility by address, which is why std_rtti::type_index uses + // std::type_index only with MSVC, and with GCC and libstdc++. bool method_identity_agrees = default_registry::rtti::type_index(exe_id) == default_registry::rtti::type_index(lib_id); @@ -82,4 +96,18 @@ BOOST_AUTO_TEST_CASE(shared_registry_state) { BOOST_TEST(std::string(lib_speak(animal)) == "?"); } +// Two classes in anonymous namespaces, one per module, with the same name. +BOOST_AUTO_TEST_CASE(classes_in_anonymous_namespaces_stay_apart) { + initialize(); + + auto lib_stray = lib_make_stray(); + auto exe_stray = make_unique_virtual(); + + BOOST_TEST(lib_stray.vptr() != exe_stray.vptr()); + BOOST_TEST(std::string(speak(lib_stray)) == "library stray"); + BOOST_TEST(std::string(lib_speak(lib_stray)) == "library stray"); + BOOST_TEST(std::string(speak(exe_stray)) == "executable stray"); + BOOST_TEST(std::string(lib_speak(exe_stray)) == "executable stray"); +} + BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_std_rtti_type_index.cpp b/test/test_std_rtti_type_index.cpp new file mode 100644 index 00000000..8b993518 --- /dev/null +++ b/test/test_std_rtti_type_index.cpp @@ -0,0 +1,94 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +// std_rtti::type_index must be equal for the copies of a class in different +// modules, and different for two classes with internal linkage that have the +// same name. Where std::type_index does not do that, it scans the mangled name +// for internal linkage. + +#include + +#define BOOST_TEST_MODULE openmethod +#include + +#include + +using boost::openmethod::default_registry; +using boost::openmethod::detail::itanium_internal_linkage; + +// Names produced by GCC, Clang, and Clang with libc++. +BOOST_AUTO_TEST_CASE(mangled_names) { + const char* shared[] = { + "6Global", + "N2ns5InnerE", + "10ZLibStream", // not `ZL`, the mark of a static entity + "12UtilityClass", // not `Ut`, the mark of an unnamed type + "3BoxISt3mapINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEESt6" + "vectorIiSaIiEESt4lessIS6_ESaISt4pairIKS6_S9_EEEE", + "3BoxINSt3__13mapINS0_12basic_stringIcNS0_11char_traitsIcEENS0_" + "9allocatorIcEEEENS0_6vectorIiNS5_IiEEEENS0_4lessIS7_EENS5_INS0_" + "4pairIKS7_SA_EEEEEEE", + "4VBoxIL5Color12EE", // an enum literal, not a static entity + "4VBoxILN2ns5ShadeE12EE", // the same, of a nested enum + "4VBoxILin3EE", // a negative integer literal + "4VBoxIXadL_Z10extern_varEEE", // the address of an extern variable + "3BoxIA10_iE", + "Z15local_in_externvE5Local", // local to a function defined once + "N5boost10openmethod6methodI22speak_boost_openmethodFvNS0_11virtual_" + "ptrI6GlobalNS0_16default_registryEvEESt3mapINSt7__cxx1112basic_" + "stringIcSt11char_traitsIcESaIcEEESt6vectorIiSaIiEESt4lessISD_ESaISt4" + "pairIKSD_SG_EEEES5_EE", + }; + + for (auto name : shared) { + BOOST_TEST_INFO(name); + BOOST_TEST(!itanium_internal_linkage(name)); + } + + const char* internal[] = { + "N12_GLOBAL__N_14AnonE", + "3BoxIN12_GLOBAL__N_14AnonEE", + "4VBoxIXadL_ZL10static_varEEE", + "ZL15local_in_staticvE5Local", + "ZN2nsL15local_in_staticEvE5Local", + "NL6lambdaMUlvE_E", // GCC: a lambda in a static variable + "3$_0", // Clang: an unnamed type, or a lambda + "10._anon_100", // GCC: an unnamed type + }; + + for (auto name : internal) { + BOOST_TEST_INFO(name); + BOOST_TEST(itanium_internal_linkage(name)); + } +} + +struct Global {}; + +struct Other {}; + +namespace { +struct Anon {}; +} // namespace + +template +auto key() { + return default_registry::rtti::type_index( + default_registry::rtti::static_type()); +} + +BOOST_AUTO_TEST_CASE(keys) { + BOOST_TEST((key() == key())); + BOOST_TEST((key() != key())); + BOOST_TEST((key() < key() || key() < key())); + +#if !BOOST_OPENMETHOD_DETAIL_STD_TYPE_INDEX + // The scanner is in use: check it on the names of this compiler. + BOOST_TEST(key().local == nullptr); + BOOST_TEST( + key().local == default_registry::rtti::static_type()); + BOOST_TEST(!itanium_internal_linkage(typeid(Global).name())); + BOOST_TEST(itanium_internal_linkage(typeid(Anon).name())); +#endif +} From 0c225375ef4514e0024c76bb1a975e89152ddd62 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Fri, 9 Oct 2026 16:40:26 -0400 Subject: [PATCH 4/9] review: fix the std_rtti scanner, class_of_vptr, and the type_index contract 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 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 --- doc/modules/ROOT/pages/custom_rtti.adoc | 7 +- include/boost/openmethod/core.hpp | 57 ++---- .../boost/openmethod/policies/static_rtti.hpp | 15 +- .../boost/openmethod/policies/std_rtti.hpp | 181 +++--------------- include/boost/openmethod/preamble.hpp | 11 +- test/test_slot_allocator.cpp | 5 +- test/test_std_rtti_type_index.cpp | 32 ++-- 7 files changed, 85 insertions(+), 223 deletions(-) diff --git a/doc/modules/ROOT/pages/custom_rtti.adoc b/doc/modules/ROOT/pages/custom_rtti.adoc index cfd0640d..657ec820 100644 --- a/doc/modules/ROOT/pages/custom_rtti.adoc +++ b/doc/modules/ROOT/pages/custom_rtti.adoc @@ -65,10 +65,13 @@ struct std_rtti : rtti { This function is optional; if it is not provided, _type_ is rendered as "type_id(_type_)". -* `type_index` returns an object that _uniquely_ identifies a class. +* `type_index` returns an object that identifies a class across modules. Some forms of RTTI (most notably, C{plus}{plus}'s `typeid` operator) do not guarantee that the type information object for a class is unique within - the same program. + the same program: there is typically one per module. The key must compare + equal for all of them, and differ for two different classes - including + two classes with the same name in different translation units, such as + classes defined in anonymous namespaces, so a name alone is not enough. This function is optional; if not provided, `type` is assumed to be unique, and used as is. diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index 0353fe03..5a7ba834 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -2256,36 +2256,14 @@ using overrider_virtual_types = boost::mp11::mp_remove< MethodParameters, OverriderParameters>, void>; -// Whether a registration of a class says that it is `declared`, or derives from -// it. Only the bases listed in the same `use_classes` are known. -template -auto is_or_derives_from(const class_info& ci, type_id declared) -> bool { - using rtti = typename Registry::rtti; - - auto key = rtti::type_index(declared); - - if (rtti::type_index(ci.type) == key) { - return true; - } - - for (auto base = ci.first_base; base != ci.last_base; ++base) { - if (rtti::type_index(*base) == key) { - return true; - } - } - - return false; -} - // The class of an object, found from its v-table pointer, for an rtti policy // that cannot tell the dynamic type. Used only to report an error. // // The object's own class always matches, because the v-table pointer came from -// its `static_vptr`. Another class may match as well: a v-table whose leading -// slots are unused is pointed to before its start, possibly at the start of -// another class' v-table. Then only a class that derives from `declared` will -// do. If that still does not settle it, `declared` is the answer: it is less -// precise, but true. +// its `static_vptr`, so a single match is the answer. Another class may match +// as well: a v-table whose leading slots are unused is pointed to before its +// start, possibly at the start of another class' v-table. Then `declared` is +// the answer: it is less precise, but true. template auto class_of_vptr(vptr_type vptr, type_id declared) -> type_id { using rtti = typename Registry::rtti; @@ -2294,30 +2272,21 @@ auto class_of_vptr(vptr_type vptr, type_id declared) -> type_id { return declared; } - for (bool derived_only : {false, true}) { - type_id found = nullptr; - bool unique = true; + type_id found = nullptr; - for (auto& ci : Registry::state().classes) { - if (*ci.static_vptr != vptr || - (derived_only && !is_or_derives_from(ci, declared))) { - continue; - } - - if (!found) { - found = ci.type; - } else if (rtti::type_index(ci.type) != rtti::type_index(found)) { - unique = false; - break; - } + for (auto& ci : Registry::state().classes) { + if (*ci.static_vptr != vptr) { + continue; } - if (found && unique) { - return found; + if (!found) { + found = ci.type; + } else if (rtti::type_index(ci.type) != rtti::type_index(found)) { + return declared; } } - return declared; + return found ? found : declared; } template diff --git a/include/boost/openmethod/policies/static_rtti.hpp b/include/boost/openmethod/policies/static_rtti.hpp index 01456f60..02e90057 100644 --- a/include/boost/openmethod/policies/static_rtti.hpp +++ b/include/boost/openmethod/policies/static_rtti.hpp @@ -64,7 +64,7 @@ struct static_rtti : rtti { //! Returns the @ref type_id of `Class`. //! //! @tparam Class A class. - template + template static auto static_type() -> type_id { static char id; @@ -159,7 +159,9 @@ constexpr auto static_rtti_shareable() -> bool { std::is_class_v || std::is_union_v || std::is_enum_v) { return std::meta::has_external_linkage(^^T); } else { - return true; + // A function or pointer type, for example, has no linkage of its + // own, but may involve a class that has internal linkage. + return !static_rtti_marked_local(static_rtti_type_name()); } #else return !static_rtti_marked_local(static_rtti_type_name()); @@ -214,13 +216,14 @@ struct named_static_rtti : rtti { //! Returns the @ref type_id of `Class`. //! //! @tparam Class A class. - template + template static auto static_type() -> type_id { + using namespace detail; + // Not const: two local classes can have the same name, and a // linker may fold identical read-only objects into one. - static detail::static_rtti_name id{ - detail::static_rtti_type_name(), - detail::static_rtti_shareable() + static static_rtti_name id{ + static_rtti_type_name(), static_rtti_shareable() }; return &id; diff --git a/include/boost/openmethod/policies/std_rtti.hpp b/include/boost/openmethod/policies/std_rtti.hpp index 245b535b..7e3781fd 100644 --- a/include/boost/openmethod/policies/std_rtti.hpp +++ b/include/boost/openmethod/policies/std_rtti.hpp @@ -9,7 +9,6 @@ #include #include -#include #include #ifndef BOOST_NO_RTTI @@ -34,144 +33,21 @@ namespace boost::openmethod::detail { -inline auto mangled_is_digit(char c) -> bool { - return c >= '0' && c <= '9'; -} - -// Whether a type name mangled per the Itanium C++ ABI designates a type that a -// different type, in another translation unit, may share the name of: one with -// internal linkage, or none. The mangling says so: +// Whether a type name mangled per the Itanium C++ ABI designates a type that +// a different type, in another translation unit, may share the name of. Only +// the marks that cannot occur in an identifier are recognized, so the answer +// does not depend on parsing the name: // -// _GLOBAL__N... begins the name of an anonymous namespace -// L at the start of a name, or in the encoding of a function -// or variable (after `Z`, or `_Z` in an expression), names a -// static one: a type local to a static function, a lambda -// in the initializer of a static variable, or a template -// argument that is the address of a static variable -// Ut an unnamed type -// $ or . in a name, Clang's or GCC's name for an unnamed type, or a -// lambda +// _GLOBAL__N the name of an anonymous namespace (a reserved identifier) +// $ Clang's name for an unnamed type or a lambda: `$_0` +// . GCC's name for an unnamed type: `._anon_0` // -// Source names are skipped by their length, so an identifier like `ZLib` is -// not taken for a mark, and so are the numbers that are not lengths. A mark -// read where there is none only keeps the copies of a class apart, as if it -// had internal linkage. +// A class local to a static function, or a template instantiated with the +// address of a static variable, is not recognized - as on MSVC, whose +// decorated names do not tell those apart either. inline auto itanium_internal_linkage(std::string_view name) -> bool { - const std::size_t n = name.size(); - std::size_t i = 0; - bool encoding = false; - bool literal_value = false; - - auto skip_number = [&]() { - if (i < n && name[i] == 'n') { - ++i; - } - - while (i < n && mangled_is_digit(name[i])) { - ++i; - } - }; - - while (i < n) { - char c = name[i]; - char next = i + 1 < n ? name[i + 1] : '\0'; - - if (mangled_is_digit(c)) { - std::size_t length = 0; - - while (i < n && mangled_is_digit(name[i])) { - length = 10 * length + std::size_t(name[i++] - '0'); - } - - auto identifier = name.substr(i, length); - - // An anonymous namespace, or a name that is not an identifier: - // `$_0` (Clang) or `._anon_0` (GCC) for an unnamed type. - if (identifier.substr(0, 10) == "_GLOBAL__N" || - identifier.find_first_of("$.") != std::string_view::npos) { - return true; - } - - i += length; - - // The value of an enum literal follows its type. - if (literal_value) { - literal_value = false; - skip_number(); - } - - continue; - } - - switch (c) { - case '$': - return true; - - case 'U': - if (next == 't') { - return true; - } - - break; - - case 'Z': - encoding = true; - break; - - case 'L': - if (mangled_is_digit(next)) { - // A static function or variable, in an encoding or at the - // start of a name; an enum literal never comes there. - if (encoding || i == 0 || name[i - 1] == 'N') { - return true; - } - - // An enum literal: L E. - literal_value = true; - } else if (next >= 'a' && next <= 'z') { - // A literal of a builtin type: L E. - i += 2; - skip_number(); - continue; - } - - break; - - case 'S': - // A substitution, S _, or an abbreviation like St. - ++i; - - while ( - i < n && - (mangled_is_digit(name[i]) || - (name[i] >= 'A' && name[i] <= 'Z'))) { - ++i; - } - - continue; - - case 'D': - // A vector type: Dv _. - if (next == 'v') { - i += 2; - skip_number(); - continue; - } - - break; - - case 'T': // a template parameter: T _ - case 'A': // an array type: A _ - case '_': // a discriminator: _ , or __ _ - ++i; - skip_number(); - continue; - } - - ++i; - } - - return false; + return name.find("_GLOBAL__N") != std::string_view::npos || + name.find_first_of("$.") != std::string_view::npos; } } // namespace boost::openmethod::detail @@ -248,22 +124,25 @@ struct std_rtti : rtti { //! program typically has one per module. `type_index` maps a `type_id` //! to a key that compares equal for all the type ids of one class, //! which is what @ref initialize uses to group the registrations coming - //! from different modules. The key must also tell apart two classes - //! that have the same name, which classes with internal linkage in - //! different translation units can. - //! - //! With MSVC, and with GCC and libstdc++, the key is a - //! `std::type_index`. Their `std::type_info` equality compares names, - //! except for classes with internal linkage, which MSVC names - //! uniquely, and which GCC marks for libstdc++ to compare by address. + //! from different modules. The key also tells apart two classes with + //! the same name in different translation units, when they are defined + //! in anonymous namespaces, or are unnamed classes or lambdas. //! - //! Elsewhere, `std::type_index` does not do. libc++ compares the + //! The type of the key is unspecified; it supports `==`, `<` and + //! `std::hash`. With MSVC, and with GCC and libstdc++, it is a + //! `std::type_index`: their `std::type_info` equality compares names, + //! except where the name of a class is shared by another one, in a + //! different translation unit. Elsewhere it is not: libc++ compares the //! `std::type_info` objects of a class with hidden visibility by //! address, so the copies in different modules are unequal, and Clang //! does not mark the classes with internal linkage for libstdc++. The - //! key is then the mangled name of the class, plus its `type_id` if - //! the mangled name shows that the class has internal linkage, or - //! none. + //! key is then the mangled name, plus the `type_id` if the mangled name + //! shows an anonymous namespace, an unnamed class, or a lambda. + //! + //! @warning Two classes with the same name, local to `static` + //! functions in different translation units, or two instantiations of + //! a template on the addresses of `static` variables with the same + //! name, may be taken for the same class. //! //! @param type A `type_id`. //! @return A key identifying the class of `type`. @@ -273,10 +152,12 @@ struct std_rtti : rtti { #if BOOST_OPENMETHOD_DETAIL_STD_TYPE_INDEX return std::type_index(info); #else + using namespace detail; + const char* name = info.name(); - return detail::type_key{ - name, detail::itanium_internal_linkage(name) ? type : nullptr + return type_key{ + name, itanium_internal_linkage(name) ? type : nullptr }; #endif } diff --git a/include/boost/openmethod/preamble.hpp b/include/boost/openmethod/preamble.hpp index b234000a..c2ebd4ad 100644 --- a/include/boost/openmethod/preamble.hpp +++ b/include/boost/openmethod/preamble.hpp @@ -622,11 +622,16 @@ struct RttiFn { template static auto type_name(type_id type, Stream& stream); - //! Returns a key that uniquely identifies a class. + //! Returns a key that identifies a class across modules. + //! + //! The key must compare equal for all the type ids of one class - one per + //! module, typically - and differ for two different classes, including + //! two classes with the same name in different translation units, such as + //! classes defined in anonymous namespaces. It must support `==`, `<` and + //! `std::hash`. //! //! @param type A `type_id`. - //! @return A unique value that identifies a class with the given - //! `type_id`. + //! @return A key that identifies the class of `type`. static auto type_index(type_id type); //! Casts an object to a type. diff --git a/test/test_slot_allocator.cpp b/test/test_slot_allocator.cpp index 55feef8f..0e4e36ea 100644 --- a/test/test_slot_allocator.cpp +++ b/test/test_slot_allocator.cpp @@ -70,8 +70,9 @@ auto sstr(const std::unordered_set& container) { template auto get_class(const Compiler& comp) { // Key through the rtti policy rather than with a raw typeid: the policy - // decides what identifies a class (std_rtti uses the mangled name, see - // std_rtti::type_index), and only it knows how to derive that from a type. + // decides what identifies a class (see std_rtti::type_index, whose key + // type depends on the platform), and only it knows how to derive that + // from a type. return comp.class_map.at( boost::openmethod::default_registry::rtti::type_index(&typeid(T))); } diff --git a/test/test_std_rtti_type_index.cpp b/test/test_std_rtti_type_index.cpp index 8b993518..5ce1adaf 100644 --- a/test/test_std_rtti_type_index.cpp +++ b/test/test_std_rtti_type_index.cpp @@ -4,9 +4,9 @@ // or copy at http://www.boost.org/LICENSE_1_0.txt) // std_rtti::type_index must be equal for the copies of a class in different -// modules, and different for two classes with internal linkage that have the -// same name. Where std::type_index does not do that, it scans the mangled name -// for internal linkage. +// modules, and different for two classes with the same name in anonymous +// namespaces, or unnamed. Where std::type_index does not do that, it looks for +// their marks in the mangled name. #include @@ -23,19 +23,21 @@ BOOST_AUTO_TEST_CASE(mangled_names) { const char* shared[] = { "6Global", "N2ns5InnerE", - "10ZLibStream", // not `ZL`, the mark of a static entity - "12UtilityClass", // not `Ut`, the mark of an unnamed type + "10ZLibStream", + "12UtilityClass", + "N3app3BoxINS_1XENS_7UtilityEE", // `Ut` after a substitution "3BoxISt3mapINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEESt6" "vectorIiSaIiEESt4lessIS6_ESaISt4pairIKS6_S9_EEEE", "3BoxINSt3__13mapINS0_12basic_stringIcNS0_11char_traitsIcEENS0_" "9allocatorIcEEEENS0_6vectorIiNS5_IiEEEENS0_4lessIS7_EENS5_INS0_" "4pairIKS7_SA_EEEEEEE", - "4VBoxIL5Color12EE", // an enum literal, not a static entity - "4VBoxILN2ns5ShadeE12EE", // the same, of a nested enum - "4VBoxILin3EE", // a negative integer literal - "4VBoxIXadL_Z10extern_varEEE", // the address of an extern variable + "4VBoxIL5Color12EE", + "4VBoxILN2ns5ShadeE12EE", + "4VBoxILin3EE", + "4VBoxIXadL_Z10extern_varEEE", + "4VBoxIXadL_Z10extern_varEEL5Color1EE", // an enum literal after `_Z` "3BoxIA10_iE", - "Z15local_in_externvE5Local", // local to a function defined once + "Z15local_in_externvE5Local", "N5boost10openmethod6methodI22speak_boost_openmethodFvNS0_11virtual_" "ptrI6GlobalNS0_16default_registryEvEESt3mapINSt7__cxx1112basic_" "stringIcSt11char_traitsIcESaIcEEESt6vectorIiSaIiEESt4lessISD_ESaISt4" @@ -50,12 +52,10 @@ BOOST_AUTO_TEST_CASE(mangled_names) { const char* internal[] = { "N12_GLOBAL__N_14AnonE", "3BoxIN12_GLOBAL__N_14AnonEE", - "4VBoxIXadL_ZL10static_varEEE", - "ZL15local_in_staticvE5Local", - "ZN2nsL15local_in_staticEvE5Local", - "NL6lambdaMUlvE_E", // GCC: a lambda in a static variable - "3$_0", // Clang: an unnamed type, or a lambda - "10._anon_100", // GCC: an unnamed type + "N3app3BoxINS_12_GLOBAL__N_11YEEE", // after a substitution + "4VBoxILN2ns5ShadeE12EN12_GLOBAL__N_11XEE", // after an enum literal + "3$_0", // Clang: an unnamed type + "10._anon_100", // GCC: an unnamed type }; for (auto name : internal) { From 4995e7a24ff3f3d6650d12722a7ccfc6d67f398b Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Fri, 9 Oct 2026 16:40:27 -0400 Subject: [PATCH 5/9] doc: rework shared_libraries.adoc for brevity 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 --- doc/modules/ROOT/pages/shared_libraries.adoc | 561 ++++++++----------- 1 file changed, 232 insertions(+), 329 deletions(-) diff --git a/doc/modules/ROOT/pages/shared_libraries.adoc b/doc/modules/ROOT/pages/shared_libraries.adoc index 96647512..6522221e 100644 --- a/doc/modules/ROOT/pages/shared_libraries.adoc +++ b/doc/modules/ROOT/pages/shared_libraries.adoc @@ -2,178 +2,129 @@ [#shared_libraries] -OpenMethod interoperates with shared libraries on Linux, other POSIX-like -platforms, and Windows. +OpenMethod works with shared libraries on Linux, other POSIX-like platforms, +and Windows. A program and its shared libraries can all contribute classes, +methods and overriders to the same registry, provided they share its state. -OpenMethod uses global data to keep track of methods, overriders and classes, -all managed by static constructors and destructors. cpp:initialize[] uses that -information to set up dispatch tables. For a program and its shared libraries -to contribute classes, methods and overriders to the same registry, that data -must be truly global: a single copy, shared by all the modules. +## Sharing a Registry -All of a registry's mutable state lives in a single variable. Sharing a -registry across modules therefore means sharing that one symbol: the _owning_ -module exports it, and every _client_ module imports it. Three macros do this. -Each takes the registry as an argument, so they can be used to manage -`default_registry`, `indirect_registry` and any custom registry: +All of a registry's mutable state lives in a single variable. One module +_owns_ it and exports it; every other module - a _client_ - imports it. Three +macros do this. Each takes the registry as an argument, so they work the same +for `default_registry`, `indirect_registry` and custom registries: [cols="2,3"] |=== | Macro | Where | xref:reference:BOOST_OPENMETHOD_IMPORT_REGISTRY.adoc[BOOST_OPENMETHOD_IMPORT_REGISTRY] -| header; every translation unit of a _client_ module +| every translation unit of a client module | xref:reference:BOOST_OPENMETHOD_EXPORT_REGISTRY.adoc[BOOST_OPENMETHOD_EXPORT_REGISTRY] -| header; every translation unit of the _owning_ module +| every translation unit of the owning module | xref:reference:BOOST_OPENMETHOD_INSTANTIATE_REGISTRY.adoc[BOOST_OPENMETHOD_INSTANTIATE_REGISTRY] -| exactly one `.cpp` of the owning module +| exactly one translation unit of the owning module |=== -The owning module uses two of them: `EXPORT` in the header its translation -units share, and `INSTANTIATE` in exactly one of them. +The examples below put `IMPORT` and `EXPORT` in a header that every +translation unit includes, and choose between them with a macro that the +owning module defines: [source,c++] ---- -// header, every translation unit of a client module -BOOST_OPENMETHOD_IMPORT_REGISTRY(boost::openmethod::default_registry); -// header, every translation unit of the owning module +#ifdef OWNS_REGISTRY_STATE BOOST_OPENMETHOD_EXPORT_REGISTRY(boost::openmethod::default_registry); -// exactly one .cpp of the owning module -BOOST_OPENMETHOD_INSTANTIATE_REGISTRY(boost::openmethod::default_registry); +#else +BOOST_OPENMETHOD_IMPORT_REGISTRY(boost::openmethod::default_registry); +#endif ---- -Use them at namespace scope, after `` has been included -and after the registry class is defined - they expand to -`registry_state`, which needs a complete type. -Everything they emit is fully qualified, so nothing need be added to +The owner then adds `INSTANTIATE` to one of its `.cpp` files. Use the macros +at namespace scope, after `` and the definition of the +registry. They emit fully qualified names, so nothing needs to be added to `namespace boost::openmethod`. -The macros exist because the underlying explicit instantiations are _not_ -portable - the two ABIs want opposite things, and getting it wrong fails on one -platform while compiling silently on the other: - -* On Windows, Cygwin and MinGW, `__declspec(dllexport)` and `extern` are - incompatible on an explicit instantiation, and MSVC rejects the combination - outright. Nor is such a declaration needed there: visibility is not a PE - concept, so the owning module's other translation units may instantiate the - state implicitly. `EXPORT` expands to nothing and `INSTANTIATE` carries the - `dllexport`. -* On ELF and Mach-O the attribute must be on the _declaration_, so that every - translation unit of the owning module pins the symbol to default visibility. - Repeating it on the definition is an error on GCC, so `INSTANTIATE` carries - no attribute there. - -WARNING: on ELF, `EXPORT` is not decoration. A translation unit of the owning -module that has neither it nor the instantiation instantiates the state -implicitly, and under `-fvisibility=hidden` that copy is module-local. Because -ELF merges COMDATs at the _most restrictive_ visibility, the merged symbol then -becomes local: the module builds, exports nothing, and clients fail to link with -an undefined reference to `registry_state<...>::st`. +WARNING: a translation unit that uses neither macro still compiles, with a +private copy of the state. In a client module, that module then stops sharing +classes, methods and overriders with the rest of the program, which shows as +`missing_class` or unexpected `bad_call` errors. In the owning module, under +`-fvisibility=hidden` on ELF, it hides the state: the module exports nothing, +and clients fail to link with an undefined reference to +`registry_state<...>::st`. Putting the macros in a header, as above, prevents +both. -Nothing else needs decoration. In particular, methods declared with -`BOOST_OPENMETHOD` require no export or import: method objects are consolidated -across modules when `initialize` runs (see <>). +Do not replace the macros with hand-written explicit instantiations. Different +platforms need different decorations, and the wrong one compiles silently on +some of them (see <>). -If a library uses open-methods as an implementation detail, and does not make -them part of its public API, it is not necessary to export the registry state. +Nothing else needs decorating. Methods, overriders and class registrations are +consolidated across modules by cpp:initialize[] (see <>). +A library that uses open-methods only internally, and not in its API, does not +need to share the state at all. ## Implicit Linking -When a program links against a shared library at build time, all the modules -are loaded before `main` starts, their static constructors run, and a single -call to `initialize` sets up the dispatch tables. The only requirement is the -one stated above: one module owns the registry state, the others import it. - -The natural arrangement is for the shared library to own the registry: the -program already links against the library, so, on Windows, the import library -dependency runs in the usual direction. The `implicit_linking` example -demonstrates this arrangement. +When a program links against a shared library at build time, every module is +loaded, and its static constructors have run, before `main` starts. One call +to `initialize` then sets up the dispatch tables. -The following header is shared between the program and the library. It defines -the class hierarchy and the `meet` method, and it is also where each module says -how it relates to the registry state: +The library owns the state. The program links against the library, and on +Windows and macOS, which resolve imported symbols at link time, the library +cannot also import the state from the program: each would have to link against +the other. In the `implicit_linking` example, the header shared by both modules +defines the classes and the `meet` method, and selects `EXPORT` or `IMPORT`: [source,c++] ---- include::{shared}/implicit_linking/animals.hpp[tag=content] ---- -The module that owns the state defines `OWNS_REGISTRY_STATE` before including -this header, which selects `EXPORT`; every other module gets `IMPORT`. The owner -also uses `INSTANTIATE`, in one of its `.cpp` files. - -TIP: each module here has a single translation unit, so on ELF the owning module -could get away with `INSTANTIATE` alone. `EXPORT` is shown because it becomes -required the moment that module gains a second translation unit - see the -warning above. Putting it in the shared header from the start makes the module -safe to grow. - -Here the library owns the state. It registers the classes, provides the default -behaviour, and emits the definition: +The library defines `OWNS_REGISTRY_STATE` and instantiates the state. It +registers the classes, and provides the default overrider: [source,c++] ---- include::{shared}/implicit_linking/extensions.cpp[tag=content] ---- -The program extends it. It adds a class the library has never heard of, two -overriders that specialise `meet`, and imports the state through the header. It -also registers the library's classes a second time - see below: +The program imports the state, and adds a class and two overriders: [source,c++] ---- include::{shared}/implicit_linking/main.cpp[tag=content] ---- -Note what `next` does here: the program's `Herbivore, Carnivore` overrider calls -the library's `Animal, Animal` overrider, so control crosses the module boundary -in the middle of a single dispatch, and `cow meets wolf` prints -`do not greet, run`. - -The program registers `Animal`, `Herbivore`, `Cow`, `Carnivore` and `Wolf`, -although the library has registered them already. That is the rule -cpp:use_classes[] states: a class must be registered in as many translation -units as it has type ids. On ELF and Mach-O it has one - the dynamic linker -merges the `type_info` objects, and the second registration is redundant but -harmless. Windows keeps one `type_info` per module, so the program's `Wolf` and -the library's `Wolf` are two different type ids, and the program creates its -`Wolf` with `new`. Without the second registration the program's id is unknown -to the registry, and the first dispatch on it fails with `unknown class Wolf`. -Registering in both modules is what makes the example portable, and costs -nothing where it is not needed. - -## Dynamic Linking - -By "dynamic linking", we mean a program loading a shared library after it has -started, and accessing its content. A common application of dynamic linking is -to implement plugin architectures. +`next` crosses the module boundary in the middle of a dispatch: the program's +`Herbivore, Carnivore` overrider calls the library's `Animal, Animal` +overrider, so `cow meets wolf` prints `do not greet, run`. -A dynamically loaded library can add classes, methods and overriders to an -existing registry. `initialize` must be called to rebuild the dispatch tables -after loading or unloading a shared library. +The program registers the library's classes again, because a class must be +registered in every module that has its own type id for it (see +cpp:use_classes[]). Where the dynamic linker merges a class's `type_info` +objects - on ELF, for a class with default visibility - the second +registration is redundant but harmless. Elsewhere, and on Windows in +particular, each module has its own `type_info`; without the second +registration, the program's `Wolf`, created with `new`, is unknown to the +registry, and dispatching on it fails with `unknown class Wolf`. -Here the program is the natural owner of the registry state: it must be able -to run before the library is loaded, and regardless of whether it is loaded at -all. +## Dynamic Linking -Let's look at an example. It shares the same `animals.hpp` as the implicit-linking -example above - same classes, same `meet` method, same `OWNS_REGISTRY_STATE` -convention - but ownership is reversed. +A program can also load a shared library after it has started - a plugin, +typically - and the library can add classes, methods and overriders to the +registry. Call `initialize` again after loading or unloading a library. -Here the main program owns the registry state, so it is `main.cpp` that defines -`OWNS_REGISTRY_STATE` and emits the `EXPORT` definition, in its only translation -unit, satisfying the exactly-one rule. It provides a catch-all overrider, then -calls the `meet` method: +The program is then the natural owner of the state: it must work before the +library is loaded, and whether or not it is. The `dynamic_loading` example uses +a header like the one above. The program defines `OWNS_REGISTRY_STATE`, +instantiates the state, provides a catch-all overrider, and calls `meet`: [source,c++] ---- include::{shared}/dynamic_loading/main.cpp[tag=before] ---- -The shared library does not define `OWNS_REGISTRY_STATE`, so the header -imported the state for it. It adds two overriders, a new class, `Tiger`, and a +The library imports the state. It adds two overriders, a class, `Tiger`, and a factory function: [source,c++] @@ -181,38 +132,35 @@ factory function: include::{shared}/dynamic_loading/extensions.cpp[tag=content] ---- -We load the shared library using Boost.DLL. After calling `initialize`, the new -overriders are installed. The main program can also use `Tiger` objects, even -though it has no knowledge of that class at compile time. +Once the library is loaded with Boost.DLL and `initialize` has run, its +overriders are in effect, and the program can use `Tiger` objects without +knowing the class: [source,c++] ---- include::{shared}/dynamic_loading/main.cpp[tag=load] ---- -Finally, we unload the shared library and call `initialize` again. The -overriders provided by the shared library are removed from the method. +Once it is unloaded and `initialize` has run again, they are gone: [source,c++] ---- include::{shared}/dynamic_loading/main.cpp[tag=unload] ---- -NOTE: `dlclose` does not necessarily unload the library when using gcc's -`-rdynamic` option. The library may remain loaded until the program exits, so -the output of the last `meet` call may still be the same as the previous one. +NOTE: with GCC's `-rdynamic`, `dlclose` does not necessarily unload the +library. It may stay loaded until the program exits, and the last `meet` call +then prints the same as the previous one. -NOTE: if a shared library installs an error handler (see -xref:error_handling.adoc[]) via the registry's `error_handler::set`, reset it -(e.g. to the previous handler) before the library is unloaded. The handler is -part of the registry's shared state, so it stays installed after `dlclose`; -calling it afterwards invokes code in the unloaded library. +NOTE: a library that installs an error handler (see +xref:error_handling.adoc[]) with the registry's `error_handler::set` must +restore the previous one before it is unloaded. The handler is part of the +shared state, and would otherwise point into unloaded code. ### CMake Setup -Since the executable owns the registry state, the shared library must link -against the executable to resolve the imported symbol. This is the reverse of -the typical linking direction: +The library imports a symbol that the executable defines, so it links against +the executable - the reverse of the usual direction: [source,cmake] ---- @@ -224,117 +172,90 @@ add_library(my_plugin SHARED extensions.cpp) target_link_libraries(my_plugin PRIVATE Boost::openmethod my_app) ---- -On Windows, `ENABLE_EXPORTS ON` on the executable tells CMake to generate an -import library (`.lib` on MSVC) that the DLL links against. On POSIX, -`ENABLE_EXPORTS` adds `-rdynamic` (or the platform's equivalent), which makes -the executable's symbols visible to the libraries it loads. - -NOTE: `BOOST_OPENMETHOD_INSTANTIATE_REGISTRY` produces an explicit instantiation -_definition_, which may appear only once in the program. Use it in exactly one -translation unit of the owning module; that module's other translation units use -`EXPORT`, and client modules `IMPORT`. - -WARNING: a translation unit that uses neither macro compiles cleanly - it just -silently gets its own private copy of the registry state instead of an error. -That module then stops sharing classes, methods and overriders with the rest of -the program (symptoms: `missing_class` or unexpected `bad_call` errors). Guard -against this by putting the applicable macro in a project header that every -translation unit includes, as in the examples, rather than repeating it in -individual `.cpp` files. +`ENABLE_EXPORTS ON` makes CMake generate an import library for the executable +on Windows, and pass `-rdynamic` (or its equivalent) on POSIX, so that the +executable's symbols are visible to the libraries it loads. [#type_ids_across_modules] ## Type Ids Across Modules -Everything above is about sharing the registry's _state_. There is a second, -quieter question: where the _type ids_ themselves come from, and how far apart -they end up. It decides how well the registry's cpp:type_hash[] policy can do -its job, and it is the one place where `dlopen` behaves differently from -ordinary linking. - -Under cpp:std_rtti[], a type id is `&typeid(X)` - the address of a -`std::type_info` object. The Itanium ABI requires that identity to be _pointer_ -identity across modules, and the linker delivers it for an implicitly linked -shared library with a copy relocation: the record is copied into the -executable's image, and the library's references are redirected to that copy. -So a program and the libraries it links against present one compact set of type -ids, however many modules there are. - -`dlopen` does not get that. A plugin's own classes are not named by the -executable, so nothing unifies them; their records stay in the plugin's own -mapping, which the loader places wherever it likes - and with address-space -randomization, somewhere different on every run. The distance between a -program's type ids and its plugin's is routinely measured in terabytes, and it -moves from run to run. - -NOTE: RTTI has to keep default visibility for ids to unify at all. Under -`-fvisibility=hidden` one class can end up with a different `type_info` object -in each module; cpp:initialize[] copes - it treats them as several ids for the -same class - but they are extra ids for the hash to separate. This is why the -library's own shared-library tests mark their classes `BOOST_SYMBOL_VISIBLE`. - -NOTE: Under cpp:static_rtti[], a type id is the address of a static variable -in a template of the library, which has hidden visibility under -`-fvisibility=hidden`, and is never shared by a Windows DLL. Each module then -has its own id for every class, and nothing tells cpp:initialize[] that they -designate the same class: it takes them for unrelated classes, and a call made -in one module on an object created in another lands in the wrong overrider. -Use cpp:named_static_rtti[] instead: it maps the ids to the names of the -classes, and cpp:initialize[] merges them. +Sharing the state is one question. Where the type ids come from is another: it +decides how well the registry's cpp:type_hash[] policy does its job, and it is +where `dlopen` differs from implicit linking. + +Under cpp:std_rtti[], a type id is `&typeid(X)`. On ELF, for an implicitly +linked library, the dynamic linker gives a class a single `type_info` across +modules, by copying the record into the executable and redirecting the +library's references to the copy. A program and its libraries thus present one +compact set of type ids. The classes of a `dlopen`{empty}ed plugin get no such +treatment: their records stay in the plugin's mapping, which the loader places +anywhere - with address-space randomization, somewhere different on every run, +and often terabytes away from the program's. + +NOTE: under `-fvisibility=hidden`, a class can have a different `type_info` in +each module. cpp:initialize[] treats them as several ids for the same class, +but each one is one more id for the hash to separate. On ELF, declaring the +class `BOOST_SYMBOL_VISIBLE` gives it a single `type_info` again. + +NOTE: under cpp:static_rtti[], a type id is the address of a static variable in +a template of the library. Under `-fvisibility=hidden`, and always with +Windows DLLs, each module has its own: cpp:initialize[] takes the copies of a +class for unrelated classes, and a call made in one module on an object created +in another lands in the wrong overrider. Use cpp:named_static_rtti[], which +maps the ids to the names of the classes so that cpp:initialize[] merges them. ### What it costs -cpp:fast_perfect_hash[], the default, searches for a multiplier `M` and a shift -`S` such that `(M * x) >> S` is collision-free over the registered type ids. It -is fast and compact when the ids are evenly spread, and degrades when they are -not - and a program plus a few `dlopen`{empty}ed modules is as uneven as it -gets: several tight clusters, very far apart. Two things follow: - -* the search gets dramatically more expensive, and on a large enough set it - fails - it gives up after half a million attempts and the error handler is - called with a `search_error`, which by default terminates the program; -* cpp:vptr_vector[] sizes its table from the hash's range, so a hash that is - working hard costs memory as well as time. +cpp:fast_perfect_hash[], the default, looks for a multiplier `M` and a shift +`S` such that `(M * x) >> S` is collision-free over the type ids. It does well +when the ids are evenly spread, and badly on a program with a few plugins: +several tight clusters, far apart. The search becomes much more expensive, and +on a large enough set it gives up after half a million attempts, calling the +error handler with a `search_error` - which terminates the program by default. +And since cpp:vptr_vector[] sizes its table from the hash's range, a struggling +hash costs memory as well as time. -A program that loads plugins and registers more than a few hundred classes is -the one most likely to meet both. +A plugin host that registers more than a few hundred classes is the program +most likely to meet both problems. A plugin that registers no classes of its +own, and only adds overriders for the program's, contributes no type ids, and +does not cause them. ### The alternatives -Three other cpp:type_hash[] policies trade that away, and a fourth option -removes the hash from the picture entirely. They are all drop-in: `with` -replaces a policy with the one of the same category, in place, so the new hash -still precedes cpp:vptr_vector[] in the list. +Three other cpp:type_hash[] policies avoid the problem, and cpp:vptr_map[] +removes the hash altogether: [cols="1,3"] |=== | policy | what it does a| cpp:minimal_perfect_hash[] -a| One slot per type id, whatever the addresses are, and a search whose cost -depends only on how many classes there are. The table size can be stated before -seeing an address. Costs a second dependent load on every dispatch - a -nanosecond or two per call. **The one to reach for in a plugin host.** +a| One slot per type id, whatever the addresses, and a search whose cost +depends only on the number of classes. Costs a second dependent load on every +dispatch - a nanosecond or two per call. **The one to reach for in a plugin +host.** a| cpp:two_level_hash[] -a| The same idea with the final reduction replaced by a shift. Cheaper per call -than `minimal_perfect_hash` where the compiler hoists the shift amount out of -the dispatch loop, at the price of a table that rounds up to a power of two - -between one and two slots per type id, depending on the class count. +a| The same idea, with the final reduction replaced by a shift. Cheaper per call +than `minimal_perfect_hash` where the compiler hoists the shift out of the +dispatch loop, but the table rounds up to a power of two: between one and two +slots per type id. a| cpp:minimal_cover_hash[] a| Indexes by the smallest set of bit positions that still separates the type -ids. As fast per call as the default, and it finds its table deterministically -in milliseconds. Needs BMI2, for **every** translation unit of the program - -see its documentation before choosing it. +ids. As fast per call as the default, and finds its table deterministically, in +milliseconds. Needs BMI2 in **every** translation unit of the program - see its +documentation before choosing it. a| cpp:vptr_map[] -a| Not a hash at all: a map keyed on the type id, so there is no table to size -and no search to fail. Slower per dispatch than any of the above, and the only -option that asks nothing of the type ids. +a| Not a hash: a map keyed on the type id, so there is no table to size and no +search to fail. Slower per dispatch than all of the above, and the only option +that asks nothing of the type ids. |=== -Switching is one declaration. The registry is then a custom registry, so it -needs the treatment in <> to be shared across modules: +Switching takes one declaration, since `with` replaces a policy with the one of +the same category, in place. The registry is then a custom registry, shared as +described in <>: [source,c++] ---- @@ -343,8 +264,8 @@ struct plugin_registry : boost::openmethod::policies::minimal_perfect_hash<>> {}; ---- -`vptr_map` replaces the `vptr` policy rather than the hash, and the hash is then -dead weight, so drop it: +`vptr_map` replaces the `vptr` policy rather than the hash, which is then dead +weight, so drop it: [source,c++] ---- @@ -354,59 +275,46 @@ struct plugin_registry : boost::openmethod::policies::type_hash> {}; ---- -TIP: none of this arises until a module registers classes of its own. A plugin -that only adds _overriders_ for classes the program already registered -contributes no new type ids, and the default policies are as good there as -anywhere. - ## Indirect Vptrs -`initialize` rebuilds the v-tables in the registry. This invalidates all the -`virtual_ptr`{empty}s, and also the v-table pointers stored in objects by -cpp:inplace_vptr_base[], related to that registry. This is seldom an issue, as -most programs that dynamically load shared libraries do so at the very beginning -of their execution. - -Otherwise, indirect v-table pointers must be used. This is achieved by using a -registry that contains the cpp:indirect_vptr[] policy. The library provides an -cpp:indirect_registry[] that has the same policies as `default_registry`, plus -`indirect_vptr`. Make it the registry the `BOOST_OPENMETHOD` macros use by -defining +`initialize` rebuilds the registry's v-tables, which invalidates its +`virtual_ptr`{empty}s, and the v-table pointers that cpp:inplace_vptr_base[] +stores in objects. Most programs load their shared libraries at the very +beginning, before any of those exist, and need not care. + +The others need a registry with the cpp:indirect_vptr[] policy, such as +cpp:indirect_registry[]: `default_registry` plus `indirect_vptr`. To make it +the registry that the `BOOST_OPENMETHOD` macros use, define xref:reference:BOOST_OPENMETHOD_DEFAULT_REGISTRY.adoc[BOOST_OPENMETHOD_DEFAULT_REGISTRY] -_before_ including ``, or any header that includes -it, like ``. +_before_ including ``, or a header that includes it, +like ``. -The `indirect_vptr` example does that in the header both modules share, rather -than passing a compiler switch from the build system. One place then settles -both questions - which registry the program uses, and how its state is shared - -and no translation unit can accidentally disagree: +The `indirect_vptr` example does it in the header that both modules share, so +that one place settles both questions - which registry, and how its state is +shared - and no translation unit can disagree. `indirect_registry` has its own +state, shared with the same three macros: [source,c++] ---- include::{shared}/indirect_vptr/animals.hpp[tag=content] ---- -cpp:indirect_registry[] has its own state, separate from that of -`default_registry`, shared with the same three macros: just name -`indirect_registry`, as above. - -Here is a program that carries `virtual_ptr`{empty}s across `initialize` calls. -It owns the state, so it defines `OWNS_REGISTRY_STATE` and emits the definition: +The program owns the state, and carries `virtual_ptr`{empty}s across +`initialize` calls: [source,c++] ---- include::{shared}/indirect_vptr/main.cpp[tag=content] ---- -The shared library it loads includes the same header, so it uses -`indirect_registry` too and imports the state. The complete example is in the -`indirect_vptr` directory. +The library it loads includes the same header, and imports the state. The +complete example is in the `indirect_vptr` directory. [#custom_registries] ## Custom Registries -A custom registry is shared exactly the same way - name it instead of -`default_registry`. Nothing above is specific to the predefined registries: +A custom registry is shared the same way - name it instead of +`default_registry`: [source,c++] ---- @@ -430,11 +338,6 @@ BOOST_OPENMETHOD_IMPORT_REGISTRY(my_registry); #endif ---- -The `#define` makes `my_registry` the registry the `BOOST_OPENMETHOD` macros -use, exactly as `indirect_vptr/animals.hpp` does above; drop it if the methods -name their registry explicitly. The sharing macros come last, after the -definition. - [source,c++] ---- // registry.cpp - exactly one translation unit of the owning module @@ -444,91 +347,91 @@ definition. BOOST_OPENMETHOD_INSTANTIATE_REGISTRY(my_registry); ---- +The `#define` makes `my_registry` the registry that the `BOOST_OPENMETHOD` +macros use; drop it if the methods name their registry explicitly. The sharing +macros come after the definition. + [#implementation_notes] ## Implementation Notes ### One state variable per registry -Everything mutable in a registry is agglomerated into a single variable. The -registry's class and method lists, the dispatch tables, and the state of every -stateful policy (for example, the hash function parameters of -cpp:fast_perfect_hash[], or the vector of v-table pointers of -cpp:vptr_vector[]) are all members of one object, of which there is exactly -one instance per registry. Policies do not keep their own globals: their state -is gathered, by template machinery, into a tuple inside that same object. +Everything mutable in a registry is a member of one object: the class and +method lists, the dispatch tables, and the state of every stateful policy - for +example, the hash parameters of cpp:fast_perfect_hash[], or the v-table +pointers of cpp:vptr_vector[]. Policies keep no globals of their own; template +machinery gathers their state into a tuple inside that object. Whatever the +policies, there is exactly _one_ symbol to export and import. -This is what makes sharing a registry across modules tractable: there is -exactly _one_ symbol to export and import, whatever the registry's policies. The variable is the sole member of cpp:registry_state[], a deliberately thin, -function-free class template. That form is dictated by MSVC: +function-free class template, a form dictated by MSVC. `dllexport`/`dllimport` are only honored on a _whole-class_ explicit -instantiation - not on a variable template, nor on the instantiation of a -lone static data member - and exporting a class wholesale also decorates its -member functions and nested types, which the state-holding class itself could -not tolerate. A one-member wrapper class with no functions is the one form -that can be exported and imported on every platform. +instantiation - not on a variable template, nor on a lone static data member - +and exporting a class decorates its member functions and nested types too, +which the class holding the state could not tolerate. A one-member class +without functions is the one form that every platform can export and import. -### Registrars are consolidated, not shared +### Registrations are consolidated, not shared -The export/import mechanism is deliberately limited to that single state -variable. No attempt is made to share the _registrars_ - the static objects +Only the state variable is shared. The _registrars_ - the static objects generated by `BOOST_OPENMETHOD`, `BOOST_OPENMETHOD_OVERRIDE` and -`BOOST_OPENMETHOD_CLASSES` - across modules. Each module keeps its own copies -of the method objects, of the registration records, and of the per-class -v-table pointers used for dispatch. +`BOOST_OPENMETHOD_CLASSES` - are not: each module keeps its own method objects, +registration records, and per-class v-table pointers. -Instead, the copies are consolidated at runtime. When a module is loaded, its +The copies are consolidated at run time instead. When a module is loaded, its static constructors append its class and method records to the lists in the -shared registry state, each record pointing back into the module that -registered it. When `initialize` runs, records that describe the same method -or class - typically, one per module, keyed by type - are grouped together. -The dispatch tables are computed once, from the consolidated information; then -the resulting slots and strides are copied back into _every_ module's copy of -each method object, and every module's v-table pointers are updated through -the pointers captured at registration time. Each module thus dispatches -through its own statics, at full speed, but all the copies agree because they -were derived from the same computation. - -This is why method declarations need no `dllexport`/`dllimport` arguments, and -why modules do not need to agree on who "owns" a method: ownership only exists -for the registry state. +shared state, each record pointing back into its module. `initialize` groups +the records that describe the same class or method, computes the dispatch +tables once, then copies the resulting slots and strides into every module's +copy of each method, and updates every module's v-table pointers through the +pointers captured at registration. Each module dispatches through its own +statics, at full speed, and all the copies agree. +This is why methods need no `dllexport` or `dllimport`, and why modules need +not agree on who "owns" a method: only the registry state has an owner. + +[#platform_details] ### Platform Details -On Windows, by default, each module (executable or DLL) receives its own copy -of global variables. Without special measures, the registry state, dispatch -tables, and method function pointers are duplicated: when a DLL's static -constructors register classes and overriders, they populate the DLL's own copy -of the registry, invisible to the main program. The `EXPORT` and `IMPORT` -macros expand to `+__declspec(dllexport)+` and `+__declspec(dllimport)+` -decorations on an explicit instantiation of the registry state, making the -owner's copy the only one. - -On Linux and other ELF platforms, the macros can sometimes be omitted. Without -them, the registry state is an implicitly instantiated template variable with -ordinary external linkage, and the dynamic linker collapses all the modules' -copies into one - provided the symbol is visible, which is what `-rdynamic` -(`ENABLE_EXPORTS ON` in CMake) ensures for an executable. However, if the -program is built with `-fvisibility=hidden`, each module's copy is internalized -to a module-local symbol, and the modules end up with separate registries, just -like on Windows. The macros handle this case: `EXPORT` emits an explicit -instantiation with default visibility; `IMPORT` emits a plain `extern template` -declaration, which suppresses the client's own instantiation, forcing it to -reference the owner's symbol. Since the macros are harmless when they are not -needed, portable code should simply always use them. - -The platforms also differ in _when_ the imported symbol must be resolved. On -ELF, a shared library may be linked with unresolved references, which the -dynamic linker satisfies at load time; a plugin can thus import the registry -state from the executable that will load it. Mach-O (macOS) and PE (Windows) -require every reference to be satisfied at link time: a client library must -link against the owning module - hence the reverse link in the CMake setup -above. - -WARNING: on Windows, every module that shares a registry must be linked -against the same dynamic C++ runtime (the `/MD` or `/MDd` family). The -registry state contains standard containers (`std::vector`, `std::function`, -etc.) allocated by whichever module runs `initialize`; if a module links the -static runtime (`/MT`) instead, or links a different runtime version, that -module's CRT frees memory it did not allocate when the state is destroyed at -exit, corrupting the heap. +On Windows, each module - executable or DLL - has its own copy of a global +variable by default. Without the macros, a DLL's static constructors would +register its classes and overriders in the DLL's own copy of the state, +invisible to the program. + +On ELF, the dynamic linker merges the modules' copies of an implicitly +instantiated variable into one - provided it is visible, which `-rdynamic` +(`ENABLE_EXPORTS ON` in CMake) ensures for an executable. Under +`-fvisibility=hidden`, each copy stays local to its module, and the modules end +up with separate registries, as on Windows. + +The two need different decorations, which is why the macros branch on the +platform: + +* On Windows, Cygwin and MinGW, `__declspec(dllexport)` and `extern` cannot be + combined on an explicit instantiation, and the owner's other translation + units may instantiate the state implicitly, since visibility is not a PE + concept. So `EXPORT` expands to nothing, and `INSTANTIATE` carries the + `dllexport`. +* On ELF and Mach-O, the attribute must be on the _declaration_, so that every + translation unit of the owner gives the symbol default visibility. GCC + rejects it if repeated on the definition, so `INSTANTIATE` carries none. + An implicit instantiation in a translation unit without `EXPORT` would hide + the symbol: ELF merges COMDATs at the _most restrictive_ visibility. + +`IMPORT` is an `extern template` declaration everywhere - with `dllimport` on +Windows - which keeps a client from instantiating the state and makes it use +the owner's. + +The platforms also differ in _when_ the imported symbol is resolved. On ELF, a +shared library may be linked with unresolved references, which the dynamic +linker satisfies at load time, so a plugin can import the state from the +executable that loads it. Mach-O and PE resolve every reference at link time, +so a client library must link against the owning module - hence the reverse +link in the CMake setup above. + +WARNING: on Windows, every module that shares a registry must use the same +dynamic C++ runtime (the `/MD` or `/MDd` family). The state contains standard +containers, allocated by whichever module runs `initialize`. A module that +links the static runtime (`/MT`), or another version of the runtime, frees +memory it did not allocate when the state is destroyed at exit, which corrupts +the heap. From 7f3b6fa5aed1464d39eb129610703ce02cfe2613 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 10 Oct 2026 11:00:21 -0400 Subject: [PATCH 6/9] simplify: named_static_rtti key, init_bad_call, tests 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 --- include/boost/openmethod/core.hpp | 15 +- .../boost/openmethod/policies/static_rtti.hpp | 47 +++--- .../boost/openmethod/policies/std_rtti.hpp | 11 +- include/boost/openmethod/preamble.hpp | 9 +- test/test_named_static_rtti.cpp | 41 ++--- test/test_static_rtti_errors.cpp | 147 +++++++++++------- 6 files changed, 147 insertions(+), 123 deletions(-) diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index 5a7ba834..dd80b1fd 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -2289,6 +2289,9 @@ auto class_of_vptr(vptr_type vptr, type_id declared) -> type_id { return found ? found : declared; } +// An rtti policy may omit `dynamic_type` (see custom_rtti.adoc). +BOOST_OPENMETHOD_DETAIL_HAS_STATIC_FN(dynamic_type); + template struct init_bad_call { template @@ -2305,14 +2308,22 @@ struct init_bad_call { if constexpr (is_virtual_ptr) { using Class = std::remove_cv_t; - if constexpr (rtti::template is_polymorphic) { + if constexpr ( + rtti::template is_polymorphic && + has_dynamic_type) { arg_type_id = rtti::dynamic_type(*arg); } else { arg_type_id = class_of_vptr( arg.vptr(), rtti::template static_type()); } - } else if constexpr (rtti::template is_polymorphic) { + } else if constexpr ( + rtti::template is_polymorphic && + has_dynamic_type) { arg_type_id = rtti::dynamic_type(arg); + } else if constexpr (has_vptr_fn) { + arg_type_id = class_of_vptr( + boost_openmethod_vptr(arg, static_cast(nullptr)), + rtti::template static_type()); } else { arg_type_id = rtti::template static_type(); } diff --git a/include/boost/openmethod/policies/static_rtti.hpp b/include/boost/openmethod/policies/static_rtti.hpp index 02e90057..772a59c3 100644 --- a/include/boost/openmethod/policies/static_rtti.hpp +++ b/include/boost/openmethod/policies/static_rtti.hpp @@ -99,12 +99,13 @@ constexpr auto static_rtti_type_name() -> std::string_view { static_rtti_prefix != std::string_view::npos, "named_static_rtti requires __PRETTY_FUNCTION__ or __FUNCSIG__"); - constexpr std::size_t suffix = - static_rtti_probe.size() - static_rtti_prefix - 4; constexpr std::string_view signature = static_rtti_signature(); + // The name of `T` takes the place of `void` in the probe. return signature.substr( - static_rtti_prefix, signature.size() - static_rtti_prefix - suffix); + static_rtti_prefix, + signature.size() - static_rtti_probe.size() + + std::string_view("void").size()); } constexpr auto static_rtti_is_qualifier(char c) -> bool { @@ -151,28 +152,22 @@ constexpr auto static_rtti_marked_local(std::string_view name) -> bool { } // Whether the copies of `T`'s type_id in different modules designate the same -// type, and can be merged by name. +// type, and can be merged by `name`, the name of `T`. Called at run time: the +// scan of the name, evaluated at compile time, would cost more than the rest of +// the registration of a class. template -constexpr auto static_rtti_shareable() -> bool { +auto static_rtti_shareable(std::string_view name) -> bool { #if BOOST_OPENMETHOD_HAS_REFLECTION if constexpr ( std::is_class_v || std::is_union_v || std::is_enum_v) { return std::meta::has_external_linkage(^^T); - } else { - // A function or pointer type, for example, has no linkage of its - // own, but may involve a class that has internal linkage. - return !static_rtti_marked_local(static_rtti_type_name()); } -#else - return !static_rtti_marked_local(static_rtti_type_name()); #endif -} -// What a named_static_rtti type_id points to. -struct static_rtti_name { - std::string_view name; - bool shareable; -}; + // Without reflection; or a function or pointer type, which has no linkage + // of its own, but may involve a class that has internal linkage. + return !static_rtti_marked_local(name); +} } // namespace boost::openmethod::detail @@ -220,10 +215,12 @@ struct named_static_rtti : rtti { static auto static_type() -> type_id { using namespace detail; - // Not const: two local classes can have the same name, and a - // linker may fold identical read-only objects into one. - static static_rtti_name id{ - static_rtti_type_name(), static_rtti_shareable() + // The type_id points to the key: the name of the class, and, if + // the class cannot be shared, the address of the key itself, + // which sets it apart from another class with the same name. + constexpr auto name = static_rtti_type_name(); + static const type_key id{ + name, static_rtti_shareable(name) ? nullptr : &id }; return &id; @@ -234,10 +231,8 @@ struct named_static_rtti : rtti { //! @param type A `type_id`. //! @return The name of the class, and `type` itself if the class //! cannot be shared. - static auto type_index(type_id type) -> detail::type_key { - auto id = static_cast(type); - - return {id->name, id->shareable ? nullptr : type}; + static auto type_index(type_id type) { + return *static_cast(type); } //! Writes the name of a class to a stream. @@ -246,7 +241,7 @@ struct named_static_rtti : rtti { //! @param stream A stream to write to. template static void type_name(type_id type, Stream& stream) { - stream << static_cast(type)->name; + stream << static_cast(type)->name; } }; }; diff --git a/include/boost/openmethod/policies/std_rtti.hpp b/include/boost/openmethod/policies/std_rtti.hpp index 7e3781fd..382458cc 100644 --- a/include/boost/openmethod/policies/std_rtti.hpp +++ b/include/boost/openmethod/policies/std_rtti.hpp @@ -21,15 +21,14 @@ // for the copies of one class in different modules, and different for two // classes with internal linkage that have the same name. See // std_rtti::type_index. -#ifndef BOOST_OPENMETHOD_DETAIL_STD_TYPE_INDEX -#if defined(_MSC_VER) || \ - (defined(__GLIBCXX__) && defined(__GNUC__) && !defined(__clang__) && \ - !defined(__INTEL_COMPILER)) +// +// _MSC_VER, not BOOST_MSVC: what matters is MSVC's standard library, which +// clang-cl uses too. +#if defined(_MSC_VER) || (defined(BOOST_GCC) && defined(__GLIBCXX__)) #define BOOST_OPENMETHOD_DETAIL_STD_TYPE_INDEX 1 #else #define BOOST_OPENMETHOD_DETAIL_STD_TYPE_INDEX 0 #endif -#endif namespace boost::openmethod::detail { @@ -154,7 +153,7 @@ struct std_rtti : rtti { #else using namespace detail; - const char* name = info.name(); + std::string_view name = info.name(); return type_key{ name, itanium_internal_linkage(name) ? type : nullptr diff --git a/include/boost/openmethod/preamble.hpp b/include/boost/openmethod/preamble.hpp index c2ebd4ad..0ef6e6ab 100644 --- a/include/boost/openmethod/preamble.hpp +++ b/include/boost/openmethod/preamble.hpp @@ -275,10 +275,11 @@ struct bad_call : openmethod_error { static constexpr std::size_t max_types = 16; //! The type_ids of the arguments. //! - //! For a class that the `rtti` policy deems polymorphic, the dynamic type - //! of the argument, as reported by the policy. Otherwise, for a - //! `virtual_ptr`, the class whose v-table it points to, or, if that is not - //! conclusive, the class of the parameter. + //! If the `rtti` policy deems the class polymorphic and provides + //! `dynamic_type`, the dynamic type of the argument, as reported by the + //! policy. Otherwise, the class whose v-table the argument uses, through a + //! `virtual_ptr` or a v-table pointer stored in the object, or, if that is + //! not conclusive, the class of the parameter. type_id types[max_types]; }; diff --git a/test/test_named_static_rtti.cpp b/test/test_named_static_rtti.cpp index 0ff3888a..f2eb5885 100644 --- a/test/test_named_static_rtti.cpp +++ b/test/test_named_static_rtti.cpp @@ -71,6 +71,7 @@ auto name() -> std::string { return os.str(); } +// std::string_view::ends_with is C++20. auto ends_with(std::string_view s, std::string_view suffix) -> bool { return s.size() >= suffix.size() && s.substr(s.size() - suffix.size()) == suffix; @@ -95,6 +96,10 @@ BOOST_AUTO_TEST_CASE(keys) { BOOST_TEST(key().local == rtti::static_type()); } +// GCC prints both classes as `local_classes::test_method()::Local`, and marks +// them as local; reflection does not need marks. Clang and MSVC leave no mark: +// see the warning in the documentation of named_static_rtti. +#if BOOST_OPENMETHOD_HAS_REFLECTION || defined(BOOST_GCC) BOOST_AUTO_TEST_CASE(local_classes) { struct Local {}; auto first = key(); @@ -103,39 +108,25 @@ BOOST_AUTO_TEST_CASE(local_classes) { struct Local {}; auto second = key(); -#if BOOST_OPENMETHOD_HAS_REFLECTION || \ - (defined(__GNUC__) && !defined(__clang__)) - // GCC prints both as `local_classes::test_method()::Local`, and marks - // them as local; reflection does not need marks. BOOST_TEST(first.local != nullptr); BOOST_TEST((first != second)); -#else - // Clang and MSVC leave no mark; see the warning in the documentation - // of named_static_rtti. - (void)first; - (void)second; -#endif } } +#endif BOOST_AUTO_TEST_CASE(dispatch) { boost::openmethod::initialize(); - { - std::stringstream os; - poke(make_unique_virtual(), os); - BOOST_TEST(os.str() == "bark"); - } - { - std::stringstream os; - poke(make_unique_virtual(), os); - BOOST_TEST(os.str() == "hiss"); - } - { - std::stringstream os; - poke(make_unique_virtual(), os); - BOOST_TEST(os.str() == "growl"); - } + auto poked = [](auto animal) { + std::ostringstream os; + poke(animal, os); + + return os.str(); + }; + + BOOST_TEST(poked(make_unique_virtual()) == "bark"); + BOOST_TEST(poked(make_unique_virtual()) == "hiss"); + BOOST_TEST(poked(make_unique_virtual()) == "growl"); } BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_static_rtti_errors.cpp b/test/test_static_rtti_errors.cpp index 089f9f05..79af5c14 100644 --- a/test/test_static_rtti_errors.cpp +++ b/test/test_static_rtti_errors.cpp @@ -10,14 +10,18 @@ #include #include #include +#include #include #include #define BOOST_TEST_MODULE openmethod #include +#include + #include -#include +#include +#include using namespace boost::openmethod; @@ -33,52 +37,29 @@ struct Dog : Animal {}; struct Cat : Animal {}; -namespace with_static_rtti { - -using R = static_registry; - -BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat, R); - -BOOST_OPENMETHOD(speak, (virtual_ptr), void); +struct BOOST_OPENMETHOD_ID(speak); +struct BOOST_OPENMETHOD_ID(meet); -BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), void) { +template +void speak_cat(virtual_ptr) { } -BOOST_OPENMETHOD(meet, (virtual_ptr, virtual_ptr), void); - -BOOST_OPENMETHOD_OVERRIDE( - meet, (virtual_ptr, virtual_ptr), void) { -} - -BOOST_OPENMETHOD_OVERRIDE( - meet, (virtual_ptr, virtual_ptr), void) { +template +void meet_dog_animal( + virtual_ptr, virtual_ptr) { } -} // namespace with_static_rtti - -namespace with_named_static_rtti { - -using R = named_registry; - -BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat, R); - -BOOST_OPENMETHOD(speak, (virtual_ptr), void); - -BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), void) { -} - -BOOST_OPENMETHOD(meet, (virtual_ptr, virtual_ptr), void); - -BOOST_OPENMETHOD_OVERRIDE( - meet, (virtual_ptr, virtual_ptr), void) { +template +void meet_animal_dog( + virtual_ptr, virtual_ptr) { } -BOOST_OPENMETHOD_OVERRIDE( - meet, (virtual_ptr, virtual_ptr), void) { +// std::string_view::ends_with is C++20. +auto ends_with(std::string_view s, std::string_view suffix) -> bool { + return s.size() >= suffix.size() && + s.substr(s.size() - suffix.size()) == suffix; } -} // namespace with_named_static_rtti - // Checks that `call` reports `Error`, and that every argument is reported as a // Dog - the class of the object, not Animal, the class of the parameter. template @@ -95,43 +76,89 @@ void check_error(Call call, std::size_t arity) { BOOST_TEST( (rtti::type_index(error.types[i]) == rtti::type_index(rtti::template static_type()))); + + if constexpr (std::is_same_v) { + // MSVC prints `struct Dog`. + std::ostringstream os; + rtti::type_name(error.types[i], os); + BOOST_TEST(ends_with(os.str(), "Dog")); + } } } catch (...) { BOOST_FAIL("wrong exception"); } } -BOOST_AUTO_TEST_CASE(static_rtti_errors) { - using namespace with_static_rtti; +using registries = boost::mp11::mp_list; + +BOOST_AUTO_TEST_CASE_TEMPLATE(bad_call_types, Registry, registries) { + BOOST_OPENMETHOD_REGISTER(use_classes); + + using speak = method< + BOOST_OPENMETHOD_ID(speak), void(virtual_ptr), + Registry>; + BOOST_OPENMETHOD_REGISTER( + typename speak::template override>); - initialize(); + using meet = method< + BOOST_OPENMETHOD_ID(meet), + void(virtual_ptr, virtual_ptr), + Registry>; + BOOST_OPENMETHOD_REGISTER( + typename meet::template override< + meet_dog_animal, meet_animal_dog>); + + initialize(); Dog dog; - check_error([&] { speak(final_virtual_ptr(dog)); }, 1); - check_error( - [&] { speak(make_unique_virtual()); }, 1); - check_error( - [&] { meet(final_virtual_ptr(dog), final_virtual_ptr(dog)); }, 2); + check_error( + [&] { speak::fn(final_virtual_ptr(dog)); }, 1); + check_error( + [&] { speak::fn(make_unique_virtual()); }, 1); + check_error( + [&] { + meet::fn( + final_virtual_ptr(dog), + final_virtual_ptr(dog)); + }, + 2); } -BOOST_AUTO_TEST_CASE(named_static_rtti_errors) { - using namespace with_named_static_rtti; +// An rtti policy that deems polymorphic classes polymorphic, but has no +// dynamic_type: classes that carry their v-table pointer do not need it. +struct inplace_rtti : policies::rtti { + template + struct fn : policies::static_rtti::fn { + template + static constexpr bool is_polymorphic = std::is_polymorphic_v; + }; +}; - initialize(); - Dog dog; +struct inplace_registry : + registry {}; + +struct Shape : inplace_vptr_base { + virtual ~Shape() = default; +}; - check_error([&] { speak(final_virtual_ptr(dog)); }, 1); - check_error( - [&] { speak(make_unique_virtual()); }, 1); - check_error( - [&] { meet(final_virtual_ptr(dog), final_virtual_ptr(dog)); }, 2); +struct Square : Shape, inplace_vptr_derived {}; + +BOOST_OPENMETHOD_CLASSES(Shape, Square, inplace_registry); + +BOOST_OPENMETHOD(draw, (virtual_), void, inplace_registry); + +BOOST_AUTO_TEST_CASE(bad_call_types_inplace_vptr) { + using rtti = inplace_registry::rtti; + + initialize(); + Square square; try { - speak(final_virtual_ptr(dog)); + draw(square); + BOOST_FAIL("should have thrown"); } catch (const no_overrider& error) { - std::ostringstream os; - R::rtti::type_name(error.types[0], os); - auto name = os.str(); - BOOST_TEST(name.substr(name.size() - 3) == "Dog"); + BOOST_TEST(error.types[0] == rtti::static_type()); + } catch (...) { + BOOST_FAIL("wrong exception"); } } From 2eb84f18a8a5f29d85091aeb7584af05ff89f41b Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 10 Oct 2026 13:03:14 -0400 Subject: [PATCH 7/9] policies: move named_static_rtti to its own header 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 --- doc/modules/ROOT/pages/ref_headers.adoc | 8 +- .../openmethod/policies/named_static_rtti.hpp | 191 ++++++++++++++++++ .../boost/openmethod/policies/static_rtti.hpp | 180 ----------------- .../named_static_rtti/registry.hpp | 2 +- test/test_named_static_rtti.cpp | 2 +- test/test_static_rtti_errors.cpp | 1 + 6 files changed, 200 insertions(+), 184 deletions(-) create mode 100644 include/boost/openmethod/policies/named_static_rtti.hpp diff --git a/doc/modules/ROOT/pages/ref_headers.adoc b/doc/modules/ROOT/pages/ref_headers.adoc index 2a0165e5..a3141682 100644 --- a/doc/modules/ROOT/pages/ref_headers.adoc +++ b/doc/modules/ROOT/pages/ref_headers.adoc @@ -165,8 +165,12 @@ is defined. That policy needs no header - see above. ### link:{headers-url}/boost/openmethod/policies/static_rtti.hpp[] Provides cpp:static_rtti[], a minimal implementation of the `rtti` policy that -does not depend on standard RTTI, and cpp:named_static_rtti[], a variant that -works across shared libraries. +does not depend on standard RTTI. + +### link:{headers-url}/boost/openmethod/policies/named_static_rtti.hpp[] + +Provides cpp:named_static_rtti[], a variant of cpp:static_rtti[] that works +across shared libraries. ### link:{headers-url}/boost/openmethod/policies/throw_error_handler.hpp[] diff --git a/include/boost/openmethod/policies/named_static_rtti.hpp b/include/boost/openmethod/policies/named_static_rtti.hpp new file mode 100644 index 00000000..fe421024 --- /dev/null +++ b/include/boost/openmethod/policies/named_static_rtti.hpp @@ -0,0 +1,191 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#ifndef BOOST_OPENMETHOD_POLICY_NAMED_STATIC_RTTI_HPP +#define BOOST_OPENMETHOD_POLICY_NAMED_STATIC_RTTI_HPP + +#include +#include + +#include + +#include +#include +#include + +namespace boost::openmethod::detail { + +// The signature of this function, as the compiler prints it, contains the name +// of `T`. Its prefix and suffix are the same for every `T`; they are measured +// on `void`. +template +constexpr auto static_rtti_signature() -> const char* { + return BOOST_CURRENT_FUNCTION; +} + +inline constexpr std::string_view static_rtti_probe = + static_rtti_signature(); +inline constexpr std::size_t static_rtti_prefix = + static_rtti_probe.find("void"); + +template +constexpr auto static_rtti_type_name() -> std::string_view { + // Without __PRETTY_FUNCTION__ or __FUNCSIG__, BOOST_CURRENT_FUNCTION falls + // back on a name that does not contain `T`, and every class would get the + // same name. + static_assert( + static_rtti_prefix != std::string_view::npos, + "named_static_rtti requires __PRETTY_FUNCTION__ or __FUNCSIG__"); + + constexpr std::string_view signature = static_rtti_signature(); + + // The name of `T` takes the place of `void` in the probe. + return signature.substr( + static_rtti_prefix, + signature.size() - static_rtti_probe.size() + + std::string_view("void").size()); +} + +constexpr auto static_rtti_is_qualifier(char c) -> bool { + return (c >= 'a' && c <= 'z') || c == ' ' || c == '&'; +} + +// A class with internal linkage, or none, may print the same name as a +// different class. Only the compilers know which classes they are; this guesses +// it from the marks they leave in the name. GCC prints a local class as +// `f()::L`, or `S::f() const::L`: a scope closed by a parenthesis, maybe +// followed by qualifiers. +constexpr auto static_rtti_marked_local(std::string_view name) -> bool { + constexpr std::string_view marks[] = { + "{anonymous}", // GCC + "(anonymous namespace)", // Clang + "`", // MSVC: `anonymous-namespace', `anonymous namespace' + " 0 && static_rtti_is_qualifier(name[i - 1])) { + --i; + } + + if (i > 0 && name[i - 1] == ')') { + return true; + } + } + + return false; +} + +// Whether the copies of `T`'s type_id in different modules designate the same +// type, and can be merged by `name`, the name of `T`. Called at run time: the +// scan of the name, evaluated at compile time, would cost more than the rest of +// the registration of a class. +template +auto static_rtti_shareable(std::string_view name) -> bool { +#if BOOST_OPENMETHOD_HAS_REFLECTION + if constexpr ( + std::is_class_v || std::is_union_v || std::is_enum_v) { + return std::meta::has_external_linkage(^^T); + } +#endif + + // Without reflection; or a function or pointer type, which has no linkage + // of its own, but may involve a class that has internal linkage. + return !static_rtti_marked_local(name); +} + +} // namespace boost::openmethod::detail + +namespace boost::openmethod::policies { + +//! Implementation of the `rtti` policy that identifies classes by name. +//! +//! `named_static_rtti` is a @ref static_rtti that works across modules. It +//! categorizes all types as non-polymorphic, and its type ids are addresses +//! that differ from one module to the next, like those of @ref static_rtti. In +//! addition, `type_index` maps them to the name of the class, as the compiler +//! prints it, so the registry merges the copies of a class coming from +//! different modules. +//! +//! All the modules must be built with the same compiler, because the names +//! must be spelled the same way. The cost is paid once, by @ref initialize. +//! Dispatch does not use names. +//! +//! A class that has internal linkage, or none, is not shared: it is identified +//! by its `type_id`, as with @ref static_rtti. This covers the classes defined +//! in an anonymous namespace, local classes, lambdas, unnamed classes, and the +//! templates instantiated with any of these. With C++26 reflection, the policy +//! asks the compiler which classes they are. Otherwise, it recognizes them from +//! the marks that the compiler leaves in their names. +//! +//! @warning Clang and MSVC leave no mark in the name of a local class. Clang +//! prints it unqualified, and MSVC prints it as if the function were a +//! namespace (`main::Local`). Without reflection, two local classes that print +//! the same name are taken for the same class, and so is a local class that +//! prints the same name as a class at namespace scope. So is a class template +//! instantiated with an entity that has internal linkage, such as the address +//! of a `static` variable, with all compilers. +//! +//! @see [Custom RTTI](xref:ROOT:custom_rtti.adoc) +struct named_static_rtti : rtti { + //! A RttiFn metafunction. + //! + //! @tparam Registry The registry containing this policy. + template + struct fn : static_rtti::fn { + //! Returns the @ref type_id of `Class`. + //! + //! @tparam Class A class. + template + static auto static_type() -> type_id { + using namespace detail; + + // The type_id points to the key: the name of the class, and, if + // the class cannot be shared, the address of the key itself, + // which sets it apart from another class with the same name. + constexpr auto name = static_rtti_type_name(); + static const type_key id{ + name, static_rtti_shareable(name) ? nullptr : &id + }; + + return &id; + } + + //! Returns a key that identifies a class across modules. + //! + //! @param type A `type_id`. + //! @return The name of the class, and `type` itself if the class + //! cannot be shared. + static auto type_index(type_id type) { + return *static_cast(type); + } + + //! Writes the name of a class to a stream. + //! + //! @param type A `type_id`. + //! @param stream A stream to write to. + template + static void type_name(type_id type, Stream& stream) { + stream << static_cast(type)->name; + } + }; +}; + +} // namespace boost::openmethod::policies + +#endif diff --git a/include/boost/openmethod/policies/static_rtti.hpp b/include/boost/openmethod/policies/static_rtti.hpp index 772a59c3..8bcdb103 100644 --- a/include/boost/openmethod/policies/static_rtti.hpp +++ b/include/boost/openmethod/policies/static_rtti.hpp @@ -7,13 +7,6 @@ #define BOOST_OPENMETHOD_POLICY_MINIMAL_RTTI_HPP #include -#include - -#include - -#include -#include -#include namespace boost::openmethod::policies { @@ -75,177 +68,4 @@ struct static_rtti : rtti { } // namespace boost::openmethod::policies -namespace boost::openmethod::detail { - -// The signature of this function, as the compiler prints it, contains the name -// of `T`. Its prefix and suffix are the same for every `T`; they are measured -// on `void`. -template -constexpr auto static_rtti_signature() -> const char* { - return BOOST_CURRENT_FUNCTION; -} - -inline constexpr std::string_view static_rtti_probe = - static_rtti_signature(); -inline constexpr std::size_t static_rtti_prefix = - static_rtti_probe.find("void"); - -template -constexpr auto static_rtti_type_name() -> std::string_view { - // Without __PRETTY_FUNCTION__ or __FUNCSIG__, BOOST_CURRENT_FUNCTION falls - // back on a name that does not contain `T`, and every class would get the - // same name. - static_assert( - static_rtti_prefix != std::string_view::npos, - "named_static_rtti requires __PRETTY_FUNCTION__ or __FUNCSIG__"); - - constexpr std::string_view signature = static_rtti_signature(); - - // The name of `T` takes the place of `void` in the probe. - return signature.substr( - static_rtti_prefix, - signature.size() - static_rtti_probe.size() + - std::string_view("void").size()); -} - -constexpr auto static_rtti_is_qualifier(char c) -> bool { - return (c >= 'a' && c <= 'z') || c == ' ' || c == '&'; -} - -// A class with internal linkage, or none, may print the same name as a -// different class. Only the compilers know which classes they are; this guesses -// it from the marks they leave in the name. GCC prints a local class as -// `f()::L`, or `S::f() const::L`: a scope closed by a parenthesis, maybe -// followed by qualifiers. -constexpr auto static_rtti_marked_local(std::string_view name) -> bool { - constexpr std::string_view marks[] = { - "{anonymous}", // GCC - "(anonymous namespace)", // Clang - "`", // MSVC: `anonymous-namespace', `anonymous namespace' - " 0 && static_rtti_is_qualifier(name[i - 1])) { - --i; - } - - if (i > 0 && name[i - 1] == ')') { - return true; - } - } - - return false; -} - -// Whether the copies of `T`'s type_id in different modules designate the same -// type, and can be merged by `name`, the name of `T`. Called at run time: the -// scan of the name, evaluated at compile time, would cost more than the rest of -// the registration of a class. -template -auto static_rtti_shareable(std::string_view name) -> bool { -#if BOOST_OPENMETHOD_HAS_REFLECTION - if constexpr ( - std::is_class_v || std::is_union_v || std::is_enum_v) { - return std::meta::has_external_linkage(^^T); - } -#endif - - // Without reflection; or a function or pointer type, which has no linkage - // of its own, but may involve a class that has internal linkage. - return !static_rtti_marked_local(name); -} - -} // namespace boost::openmethod::detail - -namespace boost::openmethod::policies { - -//! Implementation of the `rtti` policy that identifies classes by name. -//! -//! `named_static_rtti` is a @ref static_rtti that works across modules. It -//! categorizes all types as non-polymorphic, and its type ids are addresses -//! that differ from one module to the next, like those of @ref static_rtti. In -//! addition, `type_index` maps them to the name of the class, as the compiler -//! prints it, so the registry merges the copies of a class coming from -//! different modules. -//! -//! All the modules must be built with the same compiler, because the names -//! must be spelled the same way. The cost is paid once, by @ref initialize. -//! Dispatch does not use names. -//! -//! A class that has internal linkage, or none, is not shared: it is identified -//! by its `type_id`, as with @ref static_rtti. This covers the classes defined -//! in an anonymous namespace, local classes, lambdas, unnamed classes, and the -//! templates instantiated with any of these. With C++26 reflection, the policy -//! asks the compiler which classes they are. Otherwise, it recognizes them from -//! the marks that the compiler leaves in their names. -//! -//! @warning Clang and MSVC leave no mark in the name of a local class. Clang -//! prints it unqualified, and MSVC prints it as if the function were a -//! namespace (`main::Local`). Without reflection, two local classes that print -//! the same name are taken for the same class, and so is a local class that -//! prints the same name as a class at namespace scope. So is a class template -//! instantiated with an entity that has internal linkage, such as the address -//! of a `static` variable, with all compilers. -//! -//! @see [Custom RTTI](xref:ROOT:custom_rtti.adoc) -struct named_static_rtti : rtti { - //! A RttiFn metafunction. - //! - //! @tparam Registry The registry containing this policy. - template - struct fn : static_rtti::fn { - //! Returns the @ref type_id of `Class`. - //! - //! @tparam Class A class. - template - static auto static_type() -> type_id { - using namespace detail; - - // The type_id points to the key: the name of the class, and, if - // the class cannot be shared, the address of the key itself, - // which sets it apart from another class with the same name. - constexpr auto name = static_rtti_type_name(); - static const type_key id{ - name, static_rtti_shareable(name) ? nullptr : &id - }; - - return &id; - } - - //! Returns a key that identifies a class across modules. - //! - //! @param type A `type_id`. - //! @return The name of the class, and `type` itself if the class - //! cannot be shared. - static auto type_index(type_id type) { - return *static_cast(type); - } - - //! Writes the name of a class to a stream. - //! - //! @param type A `type_id`. - //! @param stream A stream to write to. - template - static void type_name(type_id type, Stream& stream) { - stream << static_cast(type)->name; - } - }; -}; - -} // namespace boost::openmethod::policies - #endif diff --git a/test/implicit_shared_libraries/named_static_rtti/registry.hpp b/test/implicit_shared_libraries/named_static_rtti/registry.hpp index 673b3ac5..0d7ae607 100644 --- a/test/implicit_shared_libraries/named_static_rtti/registry.hpp +++ b/test/implicit_shared_libraries/named_static_rtti/registry.hpp @@ -12,7 +12,7 @@ struct static_registry; #define BOOST_OPENMETHOD_DEFAULT_REGISTRY static_registry #include -#include +#include struct BOOST_SYMBOL_VISIBLE static_registry : boost::openmethod::registry< diff --git a/test/test_named_static_rtti.cpp b/test/test_named_static_rtti.cpp index f2eb5885..4c98e655 100644 --- a/test/test_named_static_rtti.cpp +++ b/test/test_named_static_rtti.cpp @@ -7,7 +7,7 @@ struct named_registry; #define BOOST_OPENMETHOD_DEFAULT_REGISTRY named_registry #include -#include +#include #include #include diff --git a/test/test_static_rtti_errors.cpp b/test/test_static_rtti_errors.cpp index 79af5c14..65667144 100644 --- a/test/test_static_rtti_errors.cpp +++ b/test/test_static_rtti_errors.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include From 1746cd093b06a0e1efe615931120ebd0a1dd7d66 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 11 Oct 2026 12:27:25 -0400 Subject: [PATCH 8/9] test: keep inplace_vptr classes non-polymorphic in test_static_rtti_errors 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 --- test/test_static_rtti_errors.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/test_static_rtti_errors.cpp b/test/test_static_rtti_errors.cpp index 65667144..e6305f9b 100644 --- a/test/test_static_rtti_errors.cpp +++ b/test/test_static_rtti_errors.cpp @@ -125,22 +125,23 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(bad_call_types, Registry, registries) { 2); } -// An rtti policy that deems polymorphic classes polymorphic, but has no -// dynamic_type: classes that carry their v-table pointer do not need it. +// An rtti policy that deems every class polymorphic, but has no dynamic_type: +// classes that carry their v-table pointer do not need it. The classes must +// not be polymorphic in the C++ sense: inplace_vptr_base's constructor casts +// `this` to the derived class, which UBSan rejects while the C++ v-table +// pointer is not set yet. struct inplace_rtti : policies::rtti { template struct fn : policies::static_rtti::fn { template - static constexpr bool is_polymorphic = std::is_polymorphic_v; + static constexpr bool is_polymorphic = true; }; }; struct inplace_registry : registry {}; -struct Shape : inplace_vptr_base { - virtual ~Shape() = default; -}; +struct Shape : inplace_vptr_base {}; struct Square : Shape, inplace_vptr_derived {}; From d55d0d49d30f6fa8afd8be948a728665a56590d1 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 11 Oct 2026 12:39:01 -0400 Subject: [PATCH 9/9] doc: RttiFn::is_polymorphic has a policy-defined value The blueprint showed `std::is_polymorphic_v` 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 --- include/boost/openmethod/preamble.hpp | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/include/boost/openmethod/preamble.hpp b/include/boost/openmethod/preamble.hpp index 0ef6e6ab..fa87415d 100644 --- a/include/boost/openmethod/preamble.hpp +++ b/include/boost/openmethod/preamble.hpp @@ -590,11 +590,31 @@ struct InitializeContext { //! Blueprint for @ref rtti metafunctions (exposition only). template struct RttiFn { - //! Tests if a class is polymorphic. + //! Tests if the policy can find the class of an object at run time. + //! + //! `true` if `Class` is polymorphic in the sense of the policy: given a + //! reference to a `Class`, `dynamic_type` finds the class of the object + //! it refers to. This need not match the C++ notion of a polymorphic + //! class. When it is `true`: + //! + //! @li `virtual_` is a valid parameter type. A class that carries + //! its own v-table pointer, see @ref inplace_vptr_base, is valid either + //! way. + //! + //! @li A @ref virtual_ptr can be constructed from a reference, a pointer + //! or a smart pointer to a `Class`. Otherwise, @ref final_virtual_ptr is + //! the only way to make one. + //! + //! @li With runtime checks, @ref final_virtual_ptr verifies that the class + //! of the object is exactly `Class`. + //! + //! The value is up to the policy, and must be a constant expression. + //! @ref std_rtti uses `std::is_polymorphic_v`; @ref static_rtti + //! deems no class polymorphic. //! //! @tparam Class A class. template - static constexpr bool is_polymorphic = std::is_polymorphic_v; + static const bool is_polymorphic; //! Returns the static @ref type_id of a type. //!