Skip to content
Closed
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
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ the license header:
`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
silently unchecked test cannot slip through. So is a marker containing a `;`:
`PASS_REGULAR_EXPRESSION` is a CMake list, so the `;` would split the regex into two alternatives
and the test would pass on either half. Write `.*` in its place. 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
Expand Down
7 changes: 7 additions & 0 deletions config/Jamfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,10 @@ project /boost/openmethod/config ;

obj has_reflection : has_reflection.cpp : <cxxflags>-freflection ;
explicit has_reflection ;

# The other probe: BMI2's pext, which only policies/minimal_cover_hash.hpp
# needs. Probing beats naming an architecture - a <architecture>x86 conditional
# does not match every toolset spelling, and a compiler that rejects -mbmi2
# outright would take the directory down with it.
obj has_bmi2 : has_bmi2.cpp : <cxxflags>-mbmi2 <include>../include ;
explicit has_bmi2 ;
23 changes: 23 additions & 0 deletions config/has_bmi2.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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)

// Probe for BMI2's parallel bit extract, compiled with -mbmi2. See ../Jamfile,
// and boost/openmethod/policies/minimal_cover_hash.hpp, which is the only part
// of the library that needs the instruction.
//
// It tests the header's own feature macro rather than the intrinsic directly:
// what the test suite needs to know is whether that header will let the policy
// be used, which is a slightly narrower question than whether some spelling of
// pext compiles.

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

#include <cstdint>

static_assert(BOOST_OPENMETHOD_HAS_PEXT);

auto probe(std::uint64_t value, std::uint64_t mask) -> std::uint64_t {
return boost::openmethod::detail::pext64(value, mask);
}
81 changes: 81 additions & 0 deletions doc/modules/ROOT/examples/rolex/8/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// 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)

// tag::content[]
#include <boost/openmethod.hpp>
#include <boost/openmethod/initialize.hpp>
#include <iostream>

class Payroll;

struct Employee {
virtual ~Employee() = default;
};

struct Salesman : Employee {
double sales = 0.0;
};

// tag::pay[]
BOOST_OPENMETHOD(
pay, (Payroll & payroll, boost::openmethod::virtual_ptr<const Employee>),
double);
// end::pay[]

// tag::payroll[]
class Payroll {
public:
double balance() const {
return balance_;
}

private:
double balance_ = 1'000'000.0;

void update_balance(double amount) {
balance_ += amount;
}

static auto pay_employee(
Payroll& payroll, boost::openmethod::virtual_ptr<const Employee>)
-> double {
double pay = 5000.0;
payroll.update_balance(-pay);
return pay;
}

static auto pay_salesman(
Payroll& payroll, boost::openmethod::virtual_ptr<const Salesman> emp)
-> double {
double base = pay_employee(payroll, emp);
double commission = emp->sales * 0.05;
payroll.update_balance(-commission);
return base + commission;
}

BOOST_OPENMETHOD_OVERRIDE_FN(
pay,
(Payroll & payroll, boost::openmethod::virtual_ptr<const Employee>),
double, &Payroll::pay_employee, &Payroll::pay_salesman);
};
// end::payroll[]

// ...and let's not forget to register the classes
BOOST_OPENMETHOD_CLASSES(Employee, Salesman);

// tag::main[]
int main() {
boost::openmethod::initialize();

Payroll payroll;
Employee bill;
Salesman bob;
bob.sales = 100'000.0;

std::cout << "pay bill: $" << pay(payroll, bill) << "\n"; // $5000
std::cout << "pay bob: $" << pay(payroll, bob) << "\n"; // 10000
std::cout << "remaining balance: $" << payroll.balance() << "\n"; // $985000
}
// end::main[]
34 changes: 34 additions & 0 deletions doc/modules/ROOT/pages/friends.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,37 @@ include::{example}/6/main.cpp[tag=payroll]
Note, however, that this makes all the overriders of _any_ `pay` method, with
any signature, in the current namespace, friends of `Payroll`. Unfortunately,
C++ does not currently allow partial specialization of friend declarations.

[#member-overriders]

When `Payroll` is a class we control, there is a simpler alternative to
`friend`: make the overriders themselves `static` member functions of
`Payroll`. Being members, they already have access to the private state of
`Payroll`, with nothing to declare:

[source,c++]
----
include::{example}/8/main.cpp[tag=pay]
----

[source,c++]
----
include::{example}/8/main.cpp[tag=payroll]
----

`&Payroll::pay_employee` and `&Payroll::pay_salesman` are ordinary `static`
member function pointers - the same shape as a free function's address - so
xref:reference:BOOST_OPENMETHOD_OVERRIDE_FN.adoc[BOOST_OPENMETHOD_OVERRIDE_FN]
can register them exactly as it would register free functions, from wherever
in the class the call is placed. `override<Fn...>` accepts more than one
function, so a single call registers both overriders for `pay`.

`pay_employee` and `pay_salesman` can be, and here are, `private`: nothing
outside `Payroll` ever names them directly, since dispatch still goes through
the free function `pay`. Unlike the `friend`-based idiom above, this does not
expose every overrider of every signature of `pay` to `Payroll` - only the
ones `Payroll` itself declares are members of it in the first place.

This does not apply when `Payroll` is a class the caller does not control -
a third-party type with no room to add a member. `friend`, as shown above,
remains the way to grant access in that case.
7 changes: 7 additions & 0 deletions doc/modules/ROOT/pages/performance.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ correct vtable. Then it stores a pointer to it in the `virtual_ptr` object,
along with a pointer to the object.footnote:[This is how Go and Rust implement
dynamic dispatch.]

The cost of that lookup belongs to the registry's cpp:type_hash[] policy, which
is cpp:fast_perfect_hash[] here as everywhere `default_registry` is used - a
multiply, a shift and a load. The alternatives in
xref:shared_libraries.adoc#type_ids_across_modules[Type Ids Across Modules] buy
a smaller or more predictable table and pay for it on this path, so the figures
below are the best case rather than the only one.

If we already have a `virtual_ptr`:

[source,c++]
Expand Down
33 changes: 31 additions & 2 deletions doc/modules/ROOT/pages/ref_headers.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,18 @@ convenient macros.
* xref:#initialize[`<boost/openmethod/initialize.hpp>`] to initialize the library.
Typically only included in the translation unit containing `main`.

The following headers make it possible to use standard smart pointers in virtual
parameters:
The following headers make it possible to use standard smart pointers with
`virtual_ptr`:

* xref:#std_shared_ptr[`<boost/openmethod/interop/std_shared_ptr.hpp>`] to use
`std::shared_ptr` in virtual parameters.

* xref:#std_unique_ptr[`<boost/openmethod/interop/std_unique_ptr.hpp>`] to use
`std::unique_ptr` in virtual parameters.

* xref:#std_weak_ptr[`<boost/openmethod/interop/std_weak_ptr.hpp>`] to track
objects with `std::weak_ptr` without losing their v-table pointer.

## High-level Headers

[#core]
Expand Down Expand Up @@ -72,6 +75,14 @@ Provides a `virtual_traits` specialization that makes it possible to use a
Provides a `virtual_traits` specialization that makes it possible to use a
`std::unique_ptr` in place of a raw pointer or reference in virtual parameters.

[#std_weak_ptr]
### link:{headers-url}/boost/openmethod/interop/std_weak_ptr.hpp[<boost/openmethod/interop/std_weak_ptr.hpp>]

Provides cpp:weak_virtual_ptr[], a class that tracks an object with a
`std::weak_ptr` and remembers its v-table pointer. It is not a `virtual_ptr`,
and cannot be used in virtual parameters; its `lock` function returns a
cpp:shared_virtual_ptr[], without a hash table lookup.

[#boost_intrusive_ptr]
### link:{headers-url}/boost/openmethod/interop/boost_intrusive_ptr.hpp[<boost/openmethod/interop/boost_intrusive_ptr.hpp>]

Expand Down Expand Up @@ -166,6 +177,24 @@ exceptions.
Provides an implementation of the `vptr` policy that stores the v-table pointers
in a map (by default a `std::map`) indexed by type ids.

### link:{headers-url}/boost/openmethod/policies/minimal_perfect_hash.hpp[<boost/openmethod/policies/minimal_perfect_hash.hpp>]

Provides an implementation of the `type_hash` policy that spends one slot per
type id whatever the type ids are, by hash and displace.

### link:{headers-url}/boost/openmethod/policies/two_level_hash.hpp[<boost/openmethod/policies/two_level_hash.hpp>]

Provides an implementation of the `type_hash` policy that indexes a power-of-two
table with a per-bucket multiplier.

### link:{headers-url}/boost/openmethod/policies/minimal_cover_hash.hpp[<boost/openmethod/policies/minimal_cover_hash.hpp>]

Provides an implementation of the `type_hash` policy that indexes by the
smallest set of bit positions that separates the type ids, extracted with
BMI2{apos}s `pext`. Requires that instruction; see
xref:shared_libraries.adoc#type_ids_across_modules[Type Ids Across Modules] for
when to prefer each of the three.

## Headers Included by Other Headers

These are the library's foundations. Every other header includes them, and a
Expand Down
2 changes: 2 additions & 0 deletions doc/modules/ROOT/pages/ref_macros.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ The following macros are for advanced uses of the library.
| Description.
| xref:reference:BOOST_OPENMETHOD_DEFAULT_REGISTRY.adoc[BOOST_OPENMETHOD_DEFAULT_REGISTRY]
| Default registry.
| xref:reference:BOOST_OPENMETHOD_OVERRIDE_FN.adoc[BOOST_OPENMETHOD_OVERRIDE_FN]
| Adds one or more existing functions to a method as overriders.
| xref:reference:BOOST_OPENMETHOD_OVERRIDER.adoc[BOOST_OPENMETHOD_OVERRIDER]
| Returns the class template specialization containing an overrider.
| xref:reference:BOOST_OPENMETHOD_OVERRIDERS.adoc[BOOST_OPENMETHOD_OVERRIDERS]
Expand Down
18 changes: 18 additions & 0 deletions doc/modules/ROOT/pages/registries_and_policies.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,24 @@ using the cpp:with[] and cpp:without[] nested templates. For example,
struct indirect_registry : default_registry::with<policies::indirect_vptr> {};
----

cpp: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 `dlopen`{empty}s class-registering modules has:
cpp:minimal_perfect_hash[] spends one slot per type id whatever the addresses
are, cpp:two_level_hash[] trades a sawtooth table size for a shorter dispatch
sequence, and cpp:minimal_cover_hash[] indexes by a minimal cover of the ids'
bits but needs BMI2. Each policy's own page has the details;
xref:shared_libraries.adoc#type_ids_across_modules[Type Ids Across Modules]
explains the situation they address and when to pick which.

Policies are implemented as unary
https://www.boost.org/doc/libs/latest/libs/mp11/doc/html/mp11.html[Boost.MP11
quoted metafunctions]. A policy is an ordinary class that contains a nested
Expand Down
109 changes: 109 additions & 0 deletions doc/modules/ROOT/pages/shared_libraries.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,114 @@ against this by putting the applicable macro in a project header that every
translation unit includes, as in the examples, rather than repeating it in
individual `.cpp` files.

[#type_ids_across_modules]
## Type Ids Across Modules

Everything above is about sharing the registry's _state_. There is a second,
quieter question: where the _type ids_ themselves come from, and how far apart
they end up. It decides how well the registry's cpp:type_hash[] policy can do
its job, and it is the one place where `dlopen` behaves differently from
ordinary linking.

Under cpp:std_rtti[], a type id is `&typeid(X)` - the address of a
`std::type_info` object. The Itanium ABI requires that identity to be _pointer_
identity across modules, and the linker delivers it for an implicitly linked
shared library with a copy relocation: the record is copied into the
executable's image, and the library's references are redirected to that copy.
So a program and the libraries it links against present one compact set of type
ids, however many modules there are.

`dlopen` does not get that. A plugin's own classes are not named by the
executable, so nothing unifies them; their records stay in the plugin's own
mapping, which the loader places wherever it likes - and with address-space
randomization, somewhere different on every run. The distance between a
program's type ids and its plugin's is routinely measured in terabytes, and it
moves from run to run.

NOTE: RTTI has to keep default visibility for ids to unify at all. Under
`-fvisibility=hidden` one class can end up with a different `type_info` object
in each module; cpp:initialize[] copes - it treats them as several ids for the
same class - but they are extra ids for the hash to separate. This is why the
library's own shared-library tests mark their classes `BOOST_SYMBOL_VISIBLE`.

### What it costs

cpp:fast_perfect_hash[], the default, searches for a multiplier `M` and a shift
`S` such that `(M * x) >> S` is collision-free over the registered type ids. It
is fast and compact when the ids are evenly spread, and degrades when they are
not - and a program plus a few `dlopen`{empty}ed modules is as uneven as it
gets: several tight clusters, very far apart. Two things follow:

* the search gets dramatically more expensive, and on a large enough set it
fails - it gives up after half a million attempts and the error handler is
called with a `search_error`, which by default terminates the program;
* cpp:vptr_vector[] sizes its table from the hash's range, so a hash that is
working hard costs memory as well as time.

A program that loads plugins and registers more than a few hundred classes is
the one most likely to meet both.

### The alternatives

Three other cpp:type_hash[] policies trade that away, and a fourth option
removes the hash from the picture entirely. They are all drop-in: `with`
replaces a policy with the one of the same category, in place, so the new hash
still precedes cpp:vptr_vector[] in the list.

[cols="1,3"]
|===
| policy | what it does

a| cpp:minimal_perfect_hash[]
a| One slot per type id, whatever the addresses are, and a search whose cost
depends only on how many classes there are. The table size can be stated before
seeing an address. Costs a second dependent load on every dispatch - a
nanosecond or two per call. **The one to reach for in a plugin host.**

a| cpp:two_level_hash[]
a| The same idea with the final reduction replaced by a shift. Cheaper per call
than `minimal_perfect_hash` where the compiler hoists the shift amount out of
the dispatch loop, at the price of a table that rounds up to a power of two -
between one and two slots per type id, depending on the class count.

a| cpp:minimal_cover_hash[]
a| Indexes by the smallest set of bit positions that still separates the type
ids. As fast per call as the default, and it finds its table deterministically
in milliseconds. Needs BMI2, for **every** translation unit of the program -
see its documentation before choosing it.

a| cpp:vptr_map[]
a| Not a hash at all: a map keyed on the type id, so there is no table to size
and no search to fail. Slower per dispatch than any of the above, and the only
option that asks nothing of the type ids.
|===

Switching is one declaration. The registry is then a custom registry, so it
needs the treatment in <<custom_registries>> to be shared across modules:

[source,c++]
----
struct plugin_registry :
boost::openmethod::default_registry::with<
boost::openmethod::policies::minimal_perfect_hash<>> {};
----

`vptr_map` replaces the `vptr` policy rather than the hash, and the hash is then
dead weight, so drop it:

[source,c++]
----
struct plugin_registry :
boost::openmethod::default_registry::with<
boost::openmethod::policies::vptr_map<>>::without<
boost::openmethod::policies::type_hash> {};
----

TIP: none of this arises until a module registers classes of its own. A plugin
that only adds _overriders_ for classes the program already registered
contributes no new type ids, and the default policies are as good there as
anywhere.

## Indirect Vptrs

`initialize` rebuilds the v-tables in the registry. This invalidates all the
Expand Down Expand Up @@ -285,6 +393,7 @@ The shared library it loads includes the same header, so it uses
`indirect_registry` too and imports the state. The complete example is in the
`indirect_vptr` directory.

[#custom_registries]
## Custom Registries

A custom registry is shared exactly the same way - name it instead of
Expand Down
Loading