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.
Each test/compile_fail_*.cpp carries the diagnostic it expects in a marker comment right after
the license header:
// Expected diagnostic, as a CMake regex (see CMakeLists.txt).
// expected-error: repeated inheritancetest/CMakeLists.txt globs compile_fail_*.cpp, extracts the regex with
MATCHES "//[ \t]*expected-error:[ \t]*([^\r\n]+)", and hands it to
openmethod_compile_fail_test as the test's PASS_REGULAR_EXPRESSION. Adding a test is dropping
in a file - no build-file edit. A file with no marker is a configure-time FATAL_ERROR, so a
silently unchecked test cannot slip through. The glob has no CONFIGURE_DEPENDS (matching the
test_*.cpp glob above it), so a new file needs a manual re-run of cmake.
Where the expected wording differs across compilers, match the common substring and say why in a
comment above the marker - no matching (clang/gcc "no matching function for call to" vs MSVC "no
matching overloaded function found"), deleted function (gcc "use of a", clang "call to", MSVC
"attempting to reference a").
Do not let the compile-fail tests regenerate the build tree concurrently. Each one runs
cmake --build on the shared tree as its test command, so when the tree is stale they all re-run
CMake at once and corrupt each other - on Ninja the losers die with failed recompaction /
FAILED: build.ninja before compiling anything, the expected diagnostic never appears, and the
test fails. It reproduces about two runs in three with touch test/CMakeLists.txt; ctest -R compile_fail -j32, and not at all on an up-to-date tree, which is why it reads as random. The
empty boost_openmethod-compile_fail_fixture target plus FIXTURES_SETUP/FIXTURES_REQUIRED
does the regeneration once, before any of them.
The Visual Studio RESOURCE_LOCK is still needed on top - do not remove it. MSBuild is not
fixable by the fixture: every one of the 24 concurrent invocations walks the same project
dependency graph and stomps the same .tlog/.lastbuildstate files, not just ZERO_CHECK's.
Measured with VS 18 2026 + ctest -R compile_fail -j32: without the lock, 7-13 of 24 fail on
every run; with it, 3/3 clean at 24s (serialized). The lock is scoped to the generator, so
Windows already runs these fully parallel under -G Ninja - 4/4 clean, 1.65s, same
cl.exe. That is the fast path on Windows; the generator cannot be defaulted from CMakeLists.txt
anyway (it is fixed before the file is read - only a preset or CMAKE_GENERATOR in the
environment can set it), and CI picks its own.
b2 is not affected - it compiles these sources as ordinary targets in its own dependency graph
instead of shelling out to a nested build (~40 runs at -j32/-j64/-j128 are clean).
b2 cannot check the message - do not try to make it. test/Jamfile already loops
(compile-fail $(src)), but Boost.Build's compile-fail only inverts the exit status: the
expect-failure-generator sets T_FLAG_FAIL_EXPECTED and the engine flips OK/FAIL
(tools/build/src/engine/make1.cpp:633). The compiler's stderr is never captured - b2 writes a
stub .o containing the literal text failed as expected and a .test containing passed.
capture-output redirects output only for run tests, and testing.jam has no output-matching
rule at all. The workarounds - a compiler wrapper behind a custom toolset instance, or a
hand-rolled make action reimplementing the compile command per toolset - are not usable in Boost
CI. Message checking lives in CMake; the markers still document the intent under b2.
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
Always show disassembly in Intel syntax, never AT&T (the toolchain's default here). Add the flag to the invocation before pasting any output:
objdump -dC -M intel --no-show-raw-insngcc -S -masm=intel/clang -S -masm=intel
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
Prose lives in doc/modules/ROOT/pages/*.adoc; explanations belong there, not in comments inside
the example sources under doc/modules/ROOT/examples/, which are pulled into the rendered page
verbatim through include::example$file.cpp[tag=content]. Pages hard-wrap at ~79 columns and use
cpp:name[] for API names that have a reference page.
Render the docs; do not just eyeball the .adoc. doc/build_antora.sh (~2 min, writes the
gitignored doc/html/) is the only way to catch markup that is silently mis-parsed — asciidoctor
emits no warning for it.
Side-by-side comparisons use a table with AsciiDoc cells. The house shape is
[cols="1,1"] + |=== with a header row (interop_any.adoc, registries_and_policies.adoc,
shared_libraries.adoc). A cell holding a block - a code listing, a nested list - must be
introduced with a|, not |. The a makes the cell content parsed as AsciiDoc; without it the
[source,...] / ---- markup renders literally, and asciidoctor says nothing.
interop_type_erasure.adoc compares two dispatch sequences that way:
[cols="1,1"]
|===
| `openmethod_vptr` | `virtual_any`
a|
[source,asm]
----
mov rax, qword ptr [rdi + 32]
----
a|
[source,asm]
----
jmp qword ptr [rax + 8*rcx]
----
|===
| starts a new cell, so cell content containing one must escape it as \|. Quick structural
check before rendering - both counts must be even, and every [source,X] must be followed by
----:
grep -c '^----$' doc/modules/ROOT/pages/<page>.adoc
grep -c '^|===$' doc/modules/ROOT/pages/<page>.adocThe backtick-apostrophe trap: never write a possessive right after a code span. Asciidoctor
parses `any`'s as ` + any + the `' curly-apostrophe shorthand, which
consumes the closing backtick; the opening one is then left unmatched and pairs with the next
backtick in the same paragraph. Two things break at once — a literal ` appears in the output,
and the following code span loses its <code> formatting:
source: is part of the `any`'s type - whereas the `typeid_of`-based dispatch above
rendered: is part of the any's type - whereas the `typeid_of-based dispatch above
Reword instead: "the reference types of the any", "separate from that of default_registry".
{apos} also works and matches the house style (shared_libraries.adoc uses {empty} for
plurals: `virtual_ptr`{empty}s), but rewording is safer and reads better. Before building:
grep -rn "\`'" doc/modules/ROOT/pages/*.adoc # must return nothingAfter building, no stray backticks should survive outside code blocks —
grep -n '\' doc/html/openmethod/.html` should only hit backticks inside C++ comments.
A registry's entire mutable state - the class/method/overrider lists plus every stateful policy's
state - lives in one variable, registry_state<Registry>::st. Sharing a registry across modules
means sharing that one symbol. Three macros do it, each taking the registry as an argument and
emitting fully qualified names, so callers never open namespace boost::openmethod:
BOOST_OPENMETHOD_IMPORT_REGISTRY(R); // header, every TU of a client module
BOOST_OPENMETHOD_EXPORT_REGISTRY(R); // header, every TU of the owning module
BOOST_OPENMETHOD_INSTANTIATE_REGISTRY(R); // exactly one .cpp of the owning moduleMethods need no decoration: method objects are consolidated across modules at initialize()
time, not shared through a symbol.
Do not hand-write the underlying explicit instantiations. They are not portable, and each
spelling compiles silently on one platform while failing on the other. On declspec platforms
__declspec(dllexport) and extern are incompatible on an explicit instantiation (MSVC warning
C4910), so EXPORT expands to nothing and INSTANTIATE carries the attribute; on ELF and Mach-O
the attribute must be on the declaration, and repeating it on the definition is an error on GCC,
so EXPORT carries it and INSTANTIATE carries none. The macros branch on BOOST_HAS_DECLSPEC.
EXPORT is load-bearing on ELF, not documentation. Under -fvisibility=hidden (e.g. the Boost
super-project's BoostRoot.cmake) an owner TU with neither EXPORT nor INSTANTIATE instantiates
the state implicitly as a COMDAT; ELF merges COMDATs at the most restrictive visibility, so the
merged symbol goes module-local, the module exports nothing, and clients fail to link.
test/implicit_shared_libraries/custom_registry/lib2.cpp exists solely to guard that path.
Registries are structs deriving from registry<Policy...>, never aliases - do not "simplify"
this. The short struct name keeps mangled names short for everything keyed on the registry
(methods, virtual_ptrs, static_vptr, registrars); an alias would expand the full policy list into
all of them. That is also why the state is keyed on Registry::registry_type - the registry<...>
base - and never on the derived struct. registry_state is likewise a deliberately thin,
function-free class: that is the only shape MSVC will export whole and import via extern template.
One self-contained example per subdirectory of doc/modules/ROOT/examples/shared_libs/; tests in
test/dynamic_loading/ (whose registry_state_id() is compared across modules to prove the state
is a single symbol) and test/implicit_shared_libraries/.
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.