Methods are scoped in a registry. A method can only reference classes in the same registry. If a class is used as a virtual parameter in methods using different registries, it must be registered with each of them.

Class templates use_classes, method, virtual_ptr, and macros BOOST_OPENMETHOD and BOOST_OPENMETHOD_CLASSES, take an additional argument, a registry class, which defaults to default_registry. The default registry can be overridden by defining the preprocessor symbol BOOST_OPENMETHOD_DEFAULT_REGISTRY before including <boost/openmethod/core.hpp> (or any header that includes it, like <boost/openmethod.hpp>). The value of the symbol is used as a default template parameter for use_classes, and for method and virtual_ptr where the classes involved declare no affinity of their own - see Registry affinity below. Once it has been included, changing BOOST_OPENMETHOD_DEFAULT_REGISTRY has no effect.

For a registry the library provides, that is the whole recipe:

#define BOOST_OPENMETHOD_DEFAULT_REGISTRY boost::openmethod::indirect_registry
#include <boost/openmethod.hpp>

A registry of your own is only declared before the include, and defined after it, where the policies it is built from are available - the library’s, and any of your own, written against them:

struct my_registry;
#define BOOST_OPENMETHOD_DEFAULT_REGISTRY my_registry

#include <boost/openmethod.hpp>
#include <boost/openmethod/policies/vptr_map.hpp>

struct my_registry
    : boost::openmethod::default_registry::with<
          boost::openmethod::policies::vptr_map<>> {};

The definition must precede the first construct that instantiates the registry: a BOOST_OPENMETHOD macro, a use_classes, a method or a virtual_ptr. The symbol must name a class, not a typedef or an alias template, and the declaration must use the same class-key as the definition. Qualify the name if it could also be found in namespace boost::openmethod - registry in particular.

Registry affinity

BOOST_OPENMETHOD_DEFAULT_REGISTRY is a whole-program answer: one registry, for every class. A class can instead name its own, by declaring a boost_openmethod_registry function that takes a pointer to it and returns the registry. The function is never called - only its return type is used - so it needs no definition:

namespace zoo {

class Animal {
  public:
    virtual ~Animal() = default;

  private:
    friend auto boost_openmethod_registry(Animal*) -> zoo_registry;
};

class Dog : public Animal {};
class Cat : public Animal {};

} // namespace zoo

The class then declares an affinity for that registry - and so does every class derived from it - and everything that mentions the class finds it. A method declared without a registry argument takes the affinity of its virtual parameters:

BOOST_OPENMETHOD_CLASSES(zoo::Animal, zoo::Dog, zoo::Cat, zoo_registry);

BOOST_OPENMETHOD(speak, (virtual_<const zoo::Animal&>), std::string);

BOOST_OPENMETHOD_OVERRIDE(speak, (const zoo::Dog&), std::string) {
    return "bark";
}

BOOST_OPENMETHOD_OVERRIDE(speak, (const zoo::Cat&), std::string) {
    return "meow";
}

…​and so does virtual_ptr, along with the smart pointer aliases and the make_*_virtual factories:

static_assert(
    std::is_same_v<virtual_ptr<zoo::Dog>, virtual_ptr<zoo::Dog, zoo_registry>>);

An affinity is inherited: declaring it for the root of a hierarchy covers every class derived from it, because the derived-to-base pointer conversion makes the root’s declaration viable. A declaration for a derived class is a better match, and wins.

Every class has a registry affinity; a class that declares none has the default affinity, BOOST_OPENMETHOD_DEFAULT_REGISTRY. A declared affinity wins over a default one, so a method may mix a class that declares one with a class that does not, and lands in the declared registry. Two virtual parameters with different declared affinities are an error, as is a registry named on the method that contradicts one of its parameters, whatever its shape. A virtual_ptr parameter contributes the affinity of its class, not the registry it spells; the two must agree.

A class can also declare its affinity with a member typedef:

namespace zoo {

struct Cage {
    using boost_openmethod_registry = zoo_registry;

    virtual ~Cage() = default;
    virtual_ptr<Cage> next;
};

} // namespace zoo

The typedef takes precedence over the function, and is inherited like any member - a derived class’s typedef hides the base’s. It is the spelling for a class that mentions virtual_ptr of itself in its own body, where the class is still incomplete: the typedef is visible from the point it is declared. Such a class has already decided to be openmethod-aware; the typedef intrudes no further.

Either way, the declaration must precede the first virtual_ptr of the class, or method taking it as a virtual parameter, that does not name a registry. The answer is remembered for the rest of the translation unit, and a class mentioned before it is complete - as a member of itself, or through a forward declaration - is asked before a declaration further down can be seen.

Declaring none is fine, and is the common case: a linked structure needs no affinity, and virtual_ptr<Node> next; inside Node gets the default registry, as it always has. Declaring one too late is an error - not at the mention, where nothing is wrong yet, but where the class is complete and its affinity matters: a virtual parameter of a method, or a class registration.

A virtual parameter either carries a registry or adopts the method’s. A virtual_ptr always carries one: it is a type in its own right, and names a registry whether or not its class declares an affinity. A virtual_ carries the registry its class declares, and adopts when the class declares none - which is what lets a method mix a class that has an affinity with one that has not. Either can be spelled: virtual_<const Animal&, zoo_registry> and virtual_ptr<Animal, zoo_registry> carry zoo_registry whatever Animal declares.

A method that names a registry requires every parameter that carries one to carry that one; the parameters that adopt go along. A method that names none requires the carriers to agree, and takes their registry - or BOOST_OPENMETHOD_DEFAULT_REGISTRY if no parameter carries one.

BOOST_OPENMETHOD_CLASSES follows the affinities too, and is stricter, because a class list has no parameter to adopt from and no spelling of its own to disambiguate with. Listing a registry settles it, and then a class that declares nothing goes along while one that declares another registry is an error. Listing none, the classes must be unanimous: all declaring the same registry, or none declaring one - in which case they are registered into BOOST_OPENMETHOD_DEFAULT_REGISTRY. Mixing a class that declares an affinity with one that does not is an error, where the same mixture in a method’s parameter list is fine.

The C++26 registrar BOOST_OPENMETHOD_REGISTER_CLASSES does not follow affinities: it registers into BOOST_OPENMETHOD_DEFAULT_REGISTRY unless its groups name a registry. Its groups may name a namespace, whose classes are only known during the scan that the choice of registry feeds.

A registry has a collection of policies. Each policy belongs to a policy category. A registry may contain at most one policy of each category. Policies control how type information is obtained, how vptrs are acquired, how errors are handled and reported, etc. While the behavior of initialize can be customized via options, policies are primarily involved in method dispatch.

Policies are placed in the boost::openmethod::policies namespace.

default_registry contains the following policies:

policy category policy role

rtti

std_rtti

provides type information for classes and objects

type_hash

fast_perfect_hash

hashes type id to an index in a vector

vptr

vptr_vector

stores vptrs in an indexed collection

error_handler

default_error_handler

calls an overridable handler function

output

stderr_output

prints diagnostics to stderr

If BOOST_OPENMETHOD_ENABLE_RUNTIME_CHECKS is defined, default_registry also contains the runtime_checks policy. This enables extra validations during method dispatch, which can detect missing class registrations that could not be caught by initialize.

The library provides another predefined registry: indirect_registry. It is useful when shared libraries are dynamically loaded at runtime, and add methods and overriders across program and shared library boundaries. See the section about shared libraries.

Registries can be created from scratch, using the registry template. Here is the definition of default_registry, copied from <boost/openmethod/default_registry.hpp>, which <boost/openmethod.hpp> always includes:

struct default_registry
    : registry<
          policies::std_rtti,
          policies::fast_perfect_hash, policies::vptr_vector,
          policies::default_error_handler,
          policies::stderr_output
#ifdef BOOST_OPENMETHOD_ENABLE_RUNTIME_CHECKS
          ,
          policies::runtime_checks
#endif
          > {
};

When defining a new registry, it is recommended to define a new class, derived from registry<…​>, rather than via a typedef, which would create excessively long symbol names and make debugging harder.

That class is a convenience, not the registry’s identity. Everything a registry owns - the class and method lists, the dispatch tables, the state of every stateful policy - is keyed on the registry<…​> specialization the class derives from, which is what its registry_type member aliases. Two classes built from the same policies, in the same order, are therefore the same registry, and share everything:

struct animals : registry<policies::std_rtti, policies::vptr_map<>> {};
struct vehicles : registry<policies::std_rtti, policies::vptr_map<>> {};

// the policy lists are identical, so this is one registry, not two
static_assert(std::is_same_v<animals::registry_type, vehicles::registry_type>);

This is worth watching for when the purpose of a second registry is to isolate a set of methods from another, since such a registry would naturally be given the same policies as the first. Registering a class or a method in either would then register it in both. To keep them apart, give each one a policy of its own:

// a policy in a category of its own, carrying nothing but a number
struct marker_category {
    using category = marker_category;
};

template<int N>
struct marker final : marker_category {
    template<class Registry>
    struct fn {};
};

struct animals : default_registry::with<marker<1>> {};
struct vehicles : default_registry::with<marker<2>> {};

static_assert(!std::is_same_v<animals::registry_type, vehicles::registry_type>);

The order of the policies matters. When initialize runs, it calls each policy’s initialize in the order the policies appear in the registry, from left to right; finalize calls each policy’s finalize in the reverse order. A policy that depends on another policy having been initialized must therefore be listed after its dependency. In particular, vptr_vector reads the state of the type_hash policy, so the type_hash policy must come before vptr_vector - as in default_registry above, where fast_perfect_hash precedes vptr_vector. This particular requirement is enforced with a static_assert.

initialize is transactional. If a policy’s initialize throws - fast_perfect_hash failing to find hash factors under throw_error_handler, say - every policy gets its previous state back, and nothing else in the registry is modified: the v-table pointers, next pointers and dispatch tables are left exactly as the call found them. That need not be a state the registry can dispatch through - after a finalize it is a torn-down one - only the state that was there before. The registry is marked as not initialized, though, since that state no longer reflects the registrations, and initialize must be called again before calling a method. Only a registry with the runtime_checks policy diagnoses a call made in the meantime; without it, the call dispatches through the previous tables, which - after a dlclose - may point into unloaded code.

A registry can also be created by copying an existing registry’s policies, using the with and without nested templates. For example, indirect_registry is a tweak of default_registry:

struct indirect_registry : default_registry::with<policies::indirect_vptr> {};

with replaces the policy of the same category where it already stands, and appends only when the registry has no policy of that category yet. That is what makes a policy swap a one-liner: default_registry::with< policies::minimal_perfect_hash<>> puts the new hash exactly where fast_perfect_hash was, still ahead of vptr_vector, so the ordering rule above is not something a caller has to think about.

The library ships four type_hash policies. fast_perfect_hash is the default and the right choice for almost every program. The others exist for the case it handles least well - type ids spread over several far-apart address ranges, which is what a program that dlopens class-registering modules has: minimal_perfect_hash spends one slot per type id whatever the addresses are, two_level_hash trades a sawtooth table size for a shorter dispatch sequence, and minimal_cover_hash indexes by a minimal cover of the ids' bits but needs BMI2. Each policy’s own page has the details; Type Ids Across Modules explains the situation they address and when to pick which.

Policies are implemented as unary Boost.MP11 quoted metafunctions. A policy is an ordinary class that contains a nested class template fn, which is instantiated by the registry, passing itself as the single template argument. The reason for this mechanism is to allow policies to have static data members, which each registry must have its own set of. vptr_vector, for example, stores v-table pointers in a std::vector. If it is used in two different registries, it needs to use two different vectors, one for each registry.