Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,57 @@ rules:
`missing template arguments` - but a name that *does* resolve would bind to the wrong type
silently. `::registry` works.

### Registry affinity

A class can name its registry instead of the program naming one for every class: declare
`auto boost_openmethod_registry(Class*) -> Registry;`, preferably as a hidden friend. The class
then *declares* an affinity for that registry, inherited by its derived classes, and
`registry_affinity<T>` reads it back. `virtual_ptr` and the smart pointer aliases default to it,
and a method declared without a registry argument takes the affinity its virtual parameters agree
on (`detail::method_registry`, driven by `detail::param_affinity` / `agreed_affinity`).

Every class has an affinity; one that declares none has the *default* affinity. Only a *declared*
affinity constrains a method, so a method may mix a class that declares one with a class that does
not - the latter yields. The catch-all `boost_openmethod_registry(...)` returns the sentinel
`detail::default_affinity`, not a registry, so an affinity declared for the default registry
itself still counts as declared; `detail::registry_affinity_aux<T>::declared` is the raw answer
(sentinel or registry) and `registry_affinity<T>` the query, which always answers a registry. A
`virtual_ptr` parameter contributes its *class's* affinity, never the registry it spells; a
spelled registry must agree with the class (`validate_method_parameter`, all three shapes).

Two spellings, looked up in this order: a member typedef `boost_openmethod_registry`
(`detail::member_registry_aux` - ordinary member lookup, so inherited, hidden by a derived class's
own, ambiguous between two bases) and the ADL overload. `inplace_vptr_base` provides the typedef.

**The answer is memoized, so every question carries a `Question` tag.** A class mentioned before
it is complete - `virtual_ptr<Node>` as a member of `Node`, or through a forward declaration - is
asked before a declaration further down its body can be seen, and an untagged class template
specialization would remember that answer for the whole TU. Refusing to answer is not an option:
`virtual_ptr<Node> next;` in a plain linked structure declares no affinity and must keep working
(`test_virtual_ptr_self_referential.cpp`, on develop since #109). So `declared_affinity_aux<Class,
asked>` is the answer that builds types, and `check_affinity<Class>` puts the same question again
under the `rechecked` tag at the points where the class must be complete anyway - a virtual
parameter in `validate_method_parameter`, a registration in `use_class_aux` - and refuses an
answer that has changed. That is where a wrong registry would do its damage. The ambiguous-bases
diagnosis in `adl_affinity` is guarded on the tag, or the recheck would repeat it.

Anchoring goes through `virtual_traits` (`detail::virtual_type<T, macro_default_registry>`), never
a bare `element_type` probe: a polymorphic class may define `element_type` and must keep its own
affinity.

Two things deliberately do **not** participate, and both are documented as such:

- `use_classes` / `BOOST_OPENMETHOD_CLASSES` still register into
`BOOST_OPENMETHOD_DEFAULT_REGISTRY` unless a registry is listed last. Registering a class that
has an affinity without naming its registry is a run-time `missing_class`, not a compile error.
- The `any` and `type_erasure` interop headers are untouched. `virtual_any<A, R>&` contributes no
affinity, so a method over one behaves exactly as before.

**A test that selects a registry through an affinity needs no PCH marker.** The scan below exists
because `BOOST_OPENMETHOD_DEFAULT_REGISTRY` must be defined before `core.hpp` is parsed, and a
force-included PCH parses it first. An affinity has no such ordering relation to the library
headers, so those tests can share the PCH - do not add a fourth marker for them.

`test/CMakeLists.txt` withholds the shared PCH from any `test_*.cpp` that overrides the
registry - a force-included PCH would still precede the `#define`. It detects them by scanning
for the token `BOOST_OPENMETHOD_DEFAULT_REGISTRY` **or** for an include of a header that
Expand Down
86 changes: 86 additions & 0 deletions doc/modules/ROOT/examples/adl_registry.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) 2017-2026 Jean-Louis Leroy
// Distributed under the Boost Software License, Version 1.0.
// See accompanying file LICENSE_1_0.txt
// or copy at http://www.boost.org/LICENSE_1_0.txt)

#include <string>

#include <boost/openmethod.hpp>
#include <boost/openmethod/initialize.hpp>

#define BOOST_TEST_MODULE adl_registry
#include <boost/test/unit_test.hpp>

// tag::registry[]
struct zoo_registry :
boost::openmethod::default_registry::with<
boost::openmethod::policies::runtime_checks> {};
// end::registry[]

using namespace boost::openmethod;

// tag::affinity[]
namespace zoo {

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

private:
// Animal - and every class derived from it - belongs to zoo_registry
friend auto boost_openmethod_registry(Animal*) -> zoo_registry;
};

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

} // namespace zoo
// end::affinity[]

// tag::methods[]
BOOST_OPENMETHOD_CLASSES(zoo::Animal, zoo::Dog, zoo::Cat, zoo_registry);

// no registry argument: speak follows Animal
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";
}
// end::methods[]

// tag::virtual_ptr[]
// ...and so does virtual_ptr
static_assert(
std::is_same_v<virtual_ptr<zoo::Dog>, virtual_ptr<zoo::Dog, zoo_registry>>);
// end::virtual_ptr[]

// tag::typedef[]
namespace zoo {

struct Cage {
using boost_openmethod_registry = zoo_registry;

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

} // namespace zoo
// end::typedef[]

static_assert(std::is_same_v<registry_affinity<zoo::Cage>, zoo_registry>);

BOOST_AUTO_TEST_CASE(adl_registry) {
// tag::call[]
initialize<zoo_registry>();

zoo::Dog spot;
zoo::Cat felix;
// end::call[]

BOOST_TEST(speak(spot) == "bark");
BOOST_TEST(speak(felix) == "meow");
}
78 changes: 78 additions & 0 deletions doc/modules/ROOT/pages/registries_and_policies.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,84 @@ 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
cpp: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:

[source,c++]
----
include::example$adl_registry.cpp[tag=affinity]
----

The class then _declares_ an affinity for that registry, and everything that
mentions the class finds it. A method declared without a registry argument takes the
affinity of its virtual parameters:

[source,c++]
----
include::example$adl_registry.cpp[tag=methods]
----

...and so does cpp:virtual_ptr[], along with the smart pointer aliases and the
`make_*_virtual` factories:

[source,c++]
----
include::example$adl_registry.cpp[tag=virtual_ptr]
----

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:

[source,c++]
----
include::example$adl_registry.cpp[tag=typedef]
----

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.

WARNING: A declared affinity does not reach
xref:reference:BOOST_OPENMETHOD_CLASSES.adoc[BOOST_OPENMETHOD_CLASSES], which
still registers into `BOOST_OPENMETHOD_DEFAULT_REGISTRY` unless a registry is
listed last. Registering a class that declares an affinity, without naming its registry,
puts the class in one registry and its methods in another - and that shows up as
a `missing_class` error at run time, not as a compile error. List the registry:
`BOOST_OPENMETHOD_CLASSES(Animal, Dog, zoo_registry)`.

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
Expand Down
6 changes: 6 additions & 0 deletions doc/modules/ROOT/pages/virtual_ptr_alt.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ v-table for the bases, just like what C++ does for its native vptrs.
`inplace_vptr_base` and `inplace_vptr_derived` are aliased in `namespace
boost::openmethod::aliases`.

`inplace_vptr_base` also declares the class's
xref:ROOT:registries_and_policies.adoc#registries_and_policies[registry affinity],
as a hidden friend - it is told the registry, and every other construct can then
find it. A method over such a hierarchy needs no registry argument, and the
program needs no `BOOST_OPENMETHOD_DEFAULT_REGISTRY` override.

An object that embeds its v-table pointer does not need to be wrapped in a
`virtual_ptr` - the two fill the same goal, fast access to the v-table
pointer - and wrapping one is rejected at compile time.
1 change: 0 additions & 1 deletion doc/mrdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ exclude-symbols:
- 'boost::openmethod::registry::initialize'
- 'boost::openmethod::registry::finalize'
- 'boost::openmethod::boost_openmethod_bases'
- 'boost::openmethod::boost_openmethod_registry'
- 'boost::openmethod::registry_state::st'

# Macros. Only the public macros carry a doc comment, and with
Expand Down
Loading
Loading