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