Skip to content

Latest commit

 

History

History
406 lines (333 loc) · 19.3 KB

File metadata and controls

406 lines (333 loc) · 19.3 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

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)

Build System

CMake Build

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
ctest

Build 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 errors
  • BOOST_SRC_DIR - Path to Boost source directory (default: ../.. or $BOOST_SRC_DIR env var)

Boost.Build (b2)

Build and test:

b2 test

Quick test (for CI):

b2 test//quick

Testing

Running All Tests (CMake)

cd build
ctest

Running a Single Test (CMake)

cd build
ctest -R test_dispatch  # Run specific test by name
# or directly
./boost_openmethod-test_dispatch

Test Structure

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

Debug Mode Features

When building in Debug mode (CMAKE_BUILD_TYPE=Debug), runtime checks are automatically enabled via BOOST_OPENMETHOD_ENABLE_RUNTIME_CHECKS.

Architecture

Layered Design

The library is structured in three conceptual layers:

  1. 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
  2. Core API (core.hpp)

    • method<Id, ReturnType(Parameters...), Registry> - Method implementation
    • virtual_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<>
  3. Macro Layer (macros.hpp)

    • BOOST_OPENMETHOD(name, params, return_type) - Declare method
    • BOOST_OPENMETHOD_OVERRIDE(name, params, return_type) - Declare overrider
    • BOOST_OPENMETHOD_CLASSES(classes...) - Register class hierarchy
    • Generates static registrar objects for automatic registration

Key Concepts

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.

Component Interaction

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.

Code Conventions

Formatting

The project uses clang-format with an LLVM-based style:

  • AlignAfterOpenBracket: AlwaysBreak
  • AllowShortFunctionsOnASingleLine: false
  • No short blocks, if statements, or loops on single lines

Compiler Requirements

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

Common Development Patterns

Working with Shared Libraries / DLL Support

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) and extern are incompatible on an explicit instantiation — MSVC emits warning C4910 and, with warnings-as- errors, fails. It is also unnecessary there, visibility not being a PE concept. So EXPORT expands to nothing (static_assert(true), to swallow the ;) and INSTANTIATE carries the dllexport.
  • 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. So EXPORT carries it and INSTANTIATE carries 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 — defines test_registry (indirect iff BOOST_OPENMETHOD_DEFAULT_REGISTRY is defined on the command line), then emits BOOST_OPENMETHOD_{EXPORT,IMPORT}_REGISTRY(test_registry) according to whether the module compiles with EXPORT_REGISTRY; defines registry_state_id()
  • classes.hppAnimal/Dog/Cat definitions (marked BOOST_SYMBOL_VISIBLE so their RTTI stays default-visibility under the hidden-visibility CMake variant below) + make_dog/make_cat
  • method.hpp — declares the speak/meet methods (no declspec arguments)
  • shared_overrider.hpp — one speak overrider for Cat, included identically by method.cpp and overrider.cpp to exercise cross-module overrider deduplication (the same overrider registered by two modules must not be treated as ambiguous)
  • registry.cpp — compiled with EXPORT_REGISTRY; the shared library that owns and exports the registry state
  • method.cpp — client (imports the registry state); defines base overriders (including the shared Cat one), exports C entry points
  • overrider.cpp — dynamically loaded at runtime; adds a Dog overrider and the shared Cat overrider
  • main.cpp — links the registry lib, dlopens the method and overrider libs, checks same_ids, calls initialize(), 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.

Custom RTTI

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

Multiple Registries

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.

File Organization

  • include/boost/openmethod/ - Public headers
    • core.hpp, macros.hpp, preamble.hpp - Main headers
    • initialize.hpp - Dispatch table construction
    • default_registry.hpp - Default policy configuration
    • detail/ - Internal implementation details
    • policies/ - Policy implementations
    • interop/ - Interoperability with other systems
  • test/ - Unit tests and compile-fail tests
  • doc/modules/ROOT/examples/ - Example programs
  • doc/modules/ROOT/pages/ - AsciiDoc documentation

Dependencies (Boost Libraries)

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)

Development Workflow

  1. Make changes to headers in include/boost/openmethod/
  2. Build tests: cmake --build build --target tests
  3. Run tests: cd build && ctest
  4. For changes affecting examples: enable BOOST_OPENMETHOD_BUILD_EXAMPLES
  5. Submit PRs against the develop branch

Important Implementation Details

Static Registration

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.

Dispatch Table Construction

The initialize() function:

  1. Collects registered classes and overriders
  2. Builds class hierarchy using provided inheritance relationships
  3. Constructs dispatch tables using perfect hashing
  4. Validates configuration (in debug mode or with runtime_checks policy)

Virtual Pointer Mechanics

virtual_ptr<T> stores both object pointer and v-table pointer. It can be constructed from:

  • Raw pointers (requires prior use_classes registration)
  • 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.

Policy State Pattern

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

  1. Declare a public struct state with the data members:
    struct state {
        detail::hash_fn fn;
        std::vector<type_id> control;
    };
  2. 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 of Registry::state(), alongside the non-template overload that returns the whole registry_state_type<Registry>) returns P::fn<Registry>::state& via detail::get (get-by-type) on the policies tuple.
  3. Use st() wherever the state is read or written: st().fn, st().control, etc. (name it st() so it does not shadow the state type).

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.