This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Boost.OpenMethod is a C++17 header-only library implementing open multi-methods (multiple dispatch). Unlike traditional virtual functions where dispatch occurs only on the first (this) parameter, open methods dispatch based on the runtime types of multiple arguments.
Key Characteristics:
- C++17 required
- Header-only library
- Part of the Boost ecosystem
- Supports both CMake and Boost.Build (b2)
Basic build:
mkdir build && cd build
cmake .. -DBOOST_SRC_DIR=/path/to/boost
cmake --build .Build with tests:
cmake .. -DBOOST_OPENMETHOD_BUILD_TESTS=ON
cmake --build . --target tests
ctestBuild with examples:
cmake .. -DBOOST_OPENMETHOD_BUILD_TESTS=ON -DBOOST_OPENMETHOD_BUILD_EXAMPLES=ON
cmake --build .Important CMake options:
BOOST_OPENMETHOD_BUILD_TESTS- Enable tests (default: ON if root project)BOOST_OPENMETHOD_BUILD_EXAMPLES- Enable examples (requires tests enabled)BOOST_OPENMETHOD_WARNINGS_AS_ERRORS- Treat warnings as errorsBOOST_SRC_DIR- Path to Boost source directory (default:../..or$BOOST_SRC_DIRenv var)
Build and test:
b2 testQuick test (for CI):
b2 test//quickcd build
ctestcd build
ctest -R test_dispatch # Run specific test by name
# or directly
./boost_openmethod-test_dispatch- Test files:
test/test_*.cpp- Standard unit tests using Boost.Test - Compile-fail tests:
test/compile_fail_*.cpp- Tests that should fail to compile - Mixed build test:
test/mix_release_debug/- Tests mixing debug/release builds - Dynamic loading test:
test/dynamic_loading/- Tests shared library support (requires Boost.DLL) - 21+ test files covering dispatch, policies, virtual_ptr, RTTI, errors, etc.
When building in Debug mode (CMAKE_BUILD_TYPE=Debug), runtime checks are automatically enabled via BOOST_OPENMETHOD_ENABLE_RUNTIME_CHECKS.
The library is structured in three conceptual layers:
-
Preamble Layer (preamble.hpp)
- Foundational types:
type_id,vptr_type,virtual_<T> - Registry and policy framework
- Error types:
not_initialized,bad_call,no_overrider,ambiguous_call, etc. - No executable dispatch code
- Foundational types:
-
Core API (core.hpp)
method<Id, ReturnType(Parameters...), Registry>- Method implementationvirtual_ptr<Class, Registry>- "Wide pointer" combining object pointer + v-table pointer- Dispatch algorithms:
resolve_uni()(single dispatch),resolve_multi_*()(multiple dispatch) - Override registration via
override_impl<> - Class registration via
use_classes<>
-
Macro Layer (macros.hpp)
BOOST_OPENMETHOD(name, params, return_type)- Declare methodBOOST_OPENMETHOD_OVERRIDE(name, params, return_type)- Declare overriderBOOST_OPENMETHOD_CLASSES(classes...)- Register class hierarchy- Generates static registrar objects for automatic registration
Open Methods: Functions where dispatch depends on runtime types of multiple parameters, not just the first.
Virtual Parameters: Parameters marked with virtual_<T> or virtual_ptr<T> that participate in dispatch.
Registries: Template-parameterized contexts holding classes, methods, and policies. Default: boost::openmethod::default_registry.
Policies: Pluggable components controlling behavior:
rtti- Type identification (std_rtti, static_rtti, custom)vptr- V-table storage (vptr_vector, vptr_map)type_hash- Type ID hashing (fast_perfect_hash with hash_fn function object)error_handler- Error handling strategy (default_error_handler, throw_error_handler)output- Diagnostic output destination (stderr_output)attributes- Visibility/DLL decoration (dllexport, dllimport, local)
Dispatch Mechanisms:
- Single dispatch: Direct v-table lookup
vtbl[slot] - Multi-dispatch: Stride-based indexing through multi-dimensional dispatch tables
virtual_ptr: A "wide pointer" combining object pointer with v-table pointer for efficient dispatch. Key for enabling dispatch on non-polymorphic or smart pointer types.
User Code → Macros → Core API → Preamble → Policies
↓
Static Registration
Static initializers generated by macros call core API functions to register
classes, methods, and overriders. The initialize() function builds dispatch
tables before first use.
The project uses clang-format with an LLVM-based style:
AlignAfterOpenBracket: AlwaysBreakAllowShortFunctionsOnASingleLine: false- No short blocks, if statements, or loops on single lines
Tests require these C++17 features (checked by Boost.Build):
- auto nontype template params
- deduction guides
- fold expressions
- if constexpr
- inline variables
- structured bindings
<charconv>,<string_view>,<variant>headers
Overview: The library supports shared library usage across modules by sharing the registry's
state through an export/import decoration of a single symbol. On Windows (and Cygwin) the decoration
is dllexport/dllimport; on ELF it is visibility("default") on the export side. In the common case
off Windows the decoration can be omitted entirely — the state then has ordinary external linkage
and is shared by the dynamic linker — but that only works if the program is not built with hidden
visibility. Under -fvisibility=hidden (e.g. the Boost super-project's BoostRoot.cmake) an
implicitly instantiated st is a COMDAT that gets internalized to a per-module local symbol, so
the export/import macros must be used on ELF too (they emit a single strong, default-visibility
explicit instantiation that the other modules import).
One shared state variable: All of a registry's mutable state — the class/method/overrider
lists and every stateful policy's state (held together in the registry_state_type::policies
tuple) — lives in a single variable, registry_state<Registry>::st of type
detail::registry_state_type<Registry>. A registry reaches it through Registry::state(). Sharing
a registry across a DLL boundary therefore means sharing this one symbol.
registry_state (in boost::openmethod) is a deliberately thin, function-free class whose only
member is the static st. It is kept separate from registry_state_type (the struct holding the
actual fields, in detail) because MSVC only honors dllexport/dllimport on a whole-class
explicit instantiation — not on a variable template (clients silently get a private copy) nor on a
static-data-member instantiation (error C2720) — and dllexporting registry_state_type directly
would also decorate its member functions and the policies' nested state types, which MSVC rejects
(error C2513). A one-member, function-free class is the only shape MSVC will export as a whole and
import via extern template.
Mechanism — extern template / explicit instantiation: the shared symbol is
registry_state<Registry::registry_type>::st, where registry_type is the registry<Policy...>
base of the registry struct (that is what registry::state() uses — never key on the derived
struct). The owning module compiles, in exactly one TU, an exported explicit instantiation
definition; clients compile an imported explicit instantiation declaration, so they reference
the owner's symbol instead of instantiating their own copy:
// owner (one TU):
template struct BOOST_SYMBOL_EXPORT registry_state<R::registry_type>;
// clients:
extern template struct BOOST_SYMBOL_IMPORT registry_state<R::registry_type>;BOOST_SYMBOL_EXPORT/BOOST_SYMBOL_IMPORT are dllexport/dllimport on Windows and
visibility("default") / empty on ELF, so the same two lines serve both platforms. This is no
longer guarded by _WIN32: on ELF the pair is what makes the state shareable under hidden
visibility.
Registries are structs, not aliases — do not "simplify" this: default_registry (and the
documented custom-registry pattern) is deliberately a struct deriving from registry<Policy...>,
never a type alias. The short struct name keeps mangled/linker names short for everything keyed on
the registry (methods, virtual_ptrs, static_vptr, registrars...); an alias would expand to the
full policy list in all of those symbols. This is also why the ::registry_type spelling in the
explicit instantiations above cannot be avoided: an explicit instantiation instantiates exactly the
specialization written, so making registry_state<default_registry> work would require
default_registry to be its base (an alias) — rejected for the mangled-name reason. Only the
shared state symbol carries the full policy list, which is accepted.
Usage: three macros, each taking the registry as an argument, so the same three serve
default_registry, indirect_registry and user-defined registries. Everything they emit is fully
qualified, so callers never open namespace boost::openmethod:
// header, every TU of a client module
BOOST_OPENMETHOD_IMPORT_REGISTRY(boost::openmethod::default_registry);
// header, every TU of the owning module
BOOST_OPENMETHOD_EXPORT_REGISTRY(boost::openmethod::default_registry);
// exactly one .cpp of the owning module
BOOST_OPENMETHOD_INSTANTIATE_REGISTRY(boost::openmethod::default_registry);The owning module uses two: EXPORT in the shared header, INSTANTIATE once. Note
::registry_type inside the expansions: the state is keyed on the registry<...> base, never the
derived struct.
Why three macros and not raw incantations — the underlying explicit instantiations are not
portable, and each spelling fails on one platform while compiling silently on the other. The
macros branch on BOOST_HAS_DECLSPEC:
- declspec platforms (Windows/Cygwin/MinGW):
__declspec(dllexport)andexternare incompatible on an explicit instantiation — MSVC emitswarning C4910and, with warnings-as- errors, fails. It is also unnecessary there, visibility not being a PE concept. SoEXPORTexpands to nothing (static_assert(true), to swallow the;) andINSTANTIATEcarries thedllexport. - ELF and Mach-O: the attribute must be on the declaration; repeating it on the definition is
error: type attributes ignored after type is already defined [-Werror=attributes]on GCC, which clang accepts silently. SoEXPORTcarries it andINSTANTIATEcarries none.
EXPORT is load-bearing on ELF, not documentation: a TU of the owning module with neither
EXPORT nor INSTANTIATE instantiates the state implicitly, and under -fvisibility=hidden that
copy is module-local. ELF merges COMDATs at the most restrictive visibility, so the merged symbol
becomes local, the module exports nothing, and clients fail to link with an undefined reference to
registry_state<...>::st. test/implicit_shared_libraries/custom_registry/lib2.cpp is a second
owner TU kept solely to guard that path. Placement within the instantiating TU does not matter
(verified with readelf under -fvisibility=hidden).
Methods need no decoration: method objects are consolidated across modules at initialize()
time, not shared via a single symbol, so BOOST_OPENMETHOD(...) takes no declspec argument.
See doc/modules/ROOT/examples/shared_libs/ — one self-contained example per subdirectory
(implicit_linking/, dynamic_loading/, indirect_vptr/), each with its own animals.hpp,
main.cpp and extensions.cpp so every file spells out its export/import macro unconditionally —
plus test/dynamic_loading/ and test/implicit_shared_libraries/ for the tests.
Dynamic Loading Test (test/dynamic_loading/): verifies that the registry state is a single
shared symbol across modules. registry_state_id() (in registry.hpp) returns the registry-state
address (test_registry::id()); main.cpp's same_ids() compares two such addresses (registry vs.
method, registry vs. overrider) and asserts they are identical. (Policy state lives inside
registry_state_type, so the registry-state address is the one shared symbol.) Files:
registry.hpp— definestest_registry(indirect iffBOOST_OPENMETHOD_DEFAULT_REGISTRYis defined on the command line), then emitsBOOST_OPENMETHOD_{EXPORT,IMPORT}_REGISTRY(test_registry)according to whether the module compiles withEXPORT_REGISTRY; definesregistry_state_id()classes.hpp—Animal/Dog/Catdefinitions (markedBOOST_SYMBOL_VISIBLEso their RTTI stays default-visibility under the hidden-visibility CMake variant below) +make_dog/make_catmethod.hpp— declares thespeak/meetmethods (no declspec arguments)shared_overrider.hpp— onespeakoverrider forCat, included identically bymethod.cppandoverrider.cppto exercise cross-module overrider deduplication (the same overrider registered by two modules must not be treated as ambiguous)registry.cpp— compiled withEXPORT_REGISTRY; the shared library that owns and exports the registry statemethod.cpp— client (imports the registry state); defines base overriders (including the shared Cat one), exports C entry pointsoverrider.cpp— dynamically loaded at runtime; adds a Dog overrider and the shared Cat overridermain.cpp— links the registry lib, dlopens the method and overrider libs, checkssame_ids, callsinitialize(), tests cross-module dispatch (including the Cat overrider-dedup and class-dedup regression checks)
CMake builds five variants: _default/_indirect (dll-owned state) and _exereg_default/
_exereg_indirect (exe-owned state), crossed with the default/indirect registry, plus _hidden_vis
(forces CXX_VISIBILITY_PRESET hidden on every target to reproduce, on a standalone build, the
configuration where augment_classes()'s class-dedup must key on (type, static_vptr) rather than
type alone). b2's Jamfile only builds the dll-owned default/indirect pair; it does not currently
have a hidden-visibility variant.
When <typeinfo> is unavailable or insufficient, use static_rtti or implement custom RTTI. See doc/modules/ROOT/examples/custom_rtti/ and policies in include/boost/openmethod/policies/.
Registries are completely independent. Use separate registries to:
- Isolate method sets
- Apply different policies to different method families
- Enable coexistence of incompatible configurations
Registry type must be specified consistently across related methods and classes.
include/boost/openmethod/- Public headerscore.hpp,macros.hpp,preamble.hpp- Main headersinitialize.hpp- Dispatch table constructiondefault_registry.hpp- Default policy configurationdetail/- Internal implementation detailspolicies/- Policy implementationsinterop/- Interoperability with other systems
test/- Unit tests and compile-fail testsdoc/modules/ROOT/examples/- Example programsdoc/modules/ROOT/pages/- AsciiDoc documentation
Required:
- Boost.Assert
- Boost.Config
- Boost.Core
- Boost.DynamicBitset
- Boost.MP11 (metaprogramming)
- Boost.Preprocessor
For testing:
- Boost.Test
- Boost.SmartPtr
For examples:
- Boost.DLL (shared library examples)
- Make changes to headers in
include/boost/openmethod/ - Build tests:
cmake --build build --target tests - Run tests:
cd build && ctest - For changes affecting examples: enable
BOOST_OPENMETHOD_BUILD_EXAMPLES - Submit PRs against the
developbranch
Classes, methods, and overriders register automatically via static constructors. This happens before main(). The initialize() function must be called before first method invocation to build dispatch tables.
The initialize() function:
- Collects registered classes and overriders
- Builds class hierarchy using provided inheritance relationships
- Constructs dispatch tables using perfect hashing
- Validates configuration (in debug mode or with runtime_checks policy)
virtual_ptr<T> stores both object pointer and v-table pointer. It can be constructed from:
- Raw pointers (requires prior
use_classesregistration) - Smart pointers (std::unique_ptr, std::shared_ptr, boost::intrusive_ptr)
- References
- Other virtual_ptr instances
The v-table pointer enables O(1) method dispatch.
Stateful policies keep their data in a nested struct state inside fn<Registry> and reach it
through the registry's shared state. registry_state_type automatically gathers every
policy's state into its policies tuple, so a policy's state is part of the single shared
registry_state<Registry>::st variable — no per-policy DLL decoration, MAKE_STATICS macro, or
id() function is needed (those were all removed).
To add state to a policy's fn<Registry>:
- Declare a public
struct statewith the data members:struct state { detail::hash_fn fn; std::vector<type_id> control; };
- Add a private accessor returning this policy's slot in the registry's tuple:
static auto& st() { return Registry::template state<fast_perfect_hash>(); }
Registry::state<P>()(a templated overload ofRegistry::state(), alongside the non-template overload that returns the wholeregistry_state_type<Registry>) returnsP::fn<Registry>::state&viadetail::get(get-by-type) on thepoliciestuple. - Use
st()wherever the state is read or written:st().fn,st().control, etc. (name itst()so it does not shadow thestatetype).
registry_state_type (in preamble.hpp) builds its policies tuple by instantiating each
policy's fn<Registry>, keeping those that have a nested state (detail::has_policy_state), and
storing one of each:
mp_apply<detail::tuple,
mp_transform<policy_state_t,
mp_filter<has_policy_state,
mp_transform_q<policy_fn_q<Registry>, Registry::policy_list>>>>detail::tuple (defined in preamble.hpp) is a minimal tuple used instead of std::tuple —
which is very expensive to instantiate with MSVC — for the policy-state tuple, the use_classes
registrar tuple, and method::override::impl. It holds each element in a tuple_element<T> base
class (flat multiple inheritance, O(1) instantiation depth); detail::get retrieves an element by
type via a base-class cast. Element types must therefore be unique: lists that may contain
duplicates (use_classes with a class listed twice, override<f, f>) are deduplicated with
mp_unique before instantiating the tuple. The initialize()/finalize() options tuple
deliberately remains std::tuple: it is a documented policy-API signature.
Only the registry itself has an id() (returning &state().classes); the dynamic_loading test
uses it directly to compare the shared state address across modules.