diff --git a/CLAUDE.md b/CLAUDE.md index 24bbe7a3..c2e7ee95 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/config/Jamfile b/config/Jamfile index a90f641f..d4f562fc 100644 --- a/config/Jamfile +++ b/config/Jamfile @@ -17,3 +17,10 @@ project /boost/openmethod/config ; obj has_reflection : has_reflection.cpp : -freflection ; explicit has_reflection ; + +# The other probe: BMI2's pext, which only policies/minimal_cover_hash.hpp +# needs. Probing beats naming an architecture - a 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 : -mbmi2 ../include ; +explicit has_bmi2 ; diff --git a/config/has_bmi2.cpp b/config/has_bmi2.cpp new file mode 100644 index 00000000..65c0f87a --- /dev/null +++ b/config/has_bmi2.cpp @@ -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 + +#include + +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); +} diff --git a/doc/modules/ROOT/examples/rolex/8/main.cpp b/doc/modules/ROOT/examples/rolex/8/main.cpp new file mode 100644 index 00000000..d1b6402f --- /dev/null +++ b/doc/modules/ROOT/examples/rolex/8/main.cpp @@ -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 +#include +#include + +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), + 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) + -> double { + double pay = 5000.0; + payroll.update_balance(-pay); + return pay; + } + + static auto pay_salesman( + Payroll& payroll, boost::openmethod::virtual_ptr 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), + 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[] diff --git a/doc/modules/ROOT/pages/friends.adoc b/doc/modules/ROOT/pages/friends.adoc index dbe82d44..2b183ee6 100644 --- a/doc/modules/ROOT/pages/friends.adoc +++ b/doc/modules/ROOT/pages/friends.adoc @@ -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` 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. diff --git a/doc/modules/ROOT/pages/performance.adoc b/doc/modules/ROOT/pages/performance.adoc index 1f14b20a..d044c4e4 100644 --- a/doc/modules/ROOT/pages/performance.adoc +++ b/doc/modules/ROOT/pages/performance.adoc @@ -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++] diff --git a/doc/modules/ROOT/pages/ref_headers.adoc b/doc/modules/ROOT/pages/ref_headers.adoc index 3003bb6b..d07713f5 100644 --- a/doc/modules/ROOT/pages/ref_headers.adoc +++ b/doc/modules/ROOT/pages/ref_headers.adoc @@ -20,8 +20,8 @@ convenient macros. * xref:#initialize[``] 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[``] to use `std::shared_ptr` in virtual parameters. @@ -29,6 +29,9 @@ parameters: * xref:#std_unique_ptr[``] to use `std::unique_ptr` in virtual parameters. +* xref:#std_weak_ptr[``] to track +objects with `std::weak_ptr` without losing their v-table pointer. + ## High-level Headers [#core] @@ -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[] + +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[] @@ -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[] + +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[] + +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[] + +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 diff --git a/doc/modules/ROOT/pages/ref_macros.adoc b/doc/modules/ROOT/pages/ref_macros.adoc index cf1513cd..5caaeddf 100644 --- a/doc/modules/ROOT/pages/ref_macros.adoc +++ b/doc/modules/ROOT/pages/ref_macros.adoc @@ -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] diff --git a/doc/modules/ROOT/pages/registries_and_policies.adoc b/doc/modules/ROOT/pages/registries_and_policies.adoc index 7620cc89..647be560 100644 --- a/doc/modules/ROOT/pages/registries_and_policies.adoc +++ b/doc/modules/ROOT/pages/registries_and_policies.adoc @@ -280,6 +280,24 @@ using the cpp:with[] and cpp:without[] nested templates. For example, struct indirect_registry : default_registry::with {}; ---- +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 diff --git a/doc/modules/ROOT/pages/shared_libraries.adoc b/doc/modules/ROOT/pages/shared_libraries.adoc index 81ff0008..bd9b26ab 100644 --- a/doc/modules/ROOT/pages/shared_libraries.adoc +++ b/doc/modules/ROOT/pages/shared_libraries.adoc @@ -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 <> 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 @@ -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 diff --git a/doc/modules/ROOT/pages/smart_pointers.adoc b/doc/modules/ROOT/pages/smart_pointers.adoc index bd19e994..cfd17665 100644 --- a/doc/modules/ROOT/pages/smart_pointers.adoc +++ b/doc/modules/ROOT/pages/smart_pointers.adoc @@ -74,3 +74,50 @@ pointers: ---- include::example$ast_unique_ptr.cpp[tag=content] ---- + +[#weak_pointers] +## Weak Pointers + +A `std::weak_ptr` observes an object without keeping it alive. Since the object +may be gone, there is nothing to dispatch on: a weak pointer cannot be used in a +virtual parameter, and neither can a `virtual_ptr` to a weak pointer. Still, an +object that is tracked by weak pointers - in a cache, an observer list, or a +back pointer - is typically an object that methods will be called on, once a +weak pointer has been locked. + +- cpp:weak_virtual_ptr[] tracks an object with a `std::weak_ptr`, and + remembers its v-table pointer + +A `weak_virtual_ptr` is a storage facility, not a `virtual_ptr`. It is +constructed from a `shared_virtual_ptr` (or from a `std::shared_ptr` or a +`std::weak_ptr`), and it remembers the v-table pointer along with the weak +pointer. It cannot be dereferenced. Its `lock` function returns a +`shared_virtual_ptr`, which can be passed to methods. Since the v-table pointer +is copied, not looked up, `lock` costs no more than `std::weak_ptr::lock`. It +returns an empty `shared_virtual_ptr` if the object no longer exists. + +[source,c++] +---- +shared_virtual_ptr animal = make_shared_virtual(); +weak_virtual_ptr observer = animal; + +std::cout << poke(observer.lock()) << "\n"; // bark + +animal = nullptr; +std::cout << std::boolalpha << observer.expired() << "\n"; // true +---- + +Remembering the v-table pointer is safe with respect to the lifetime of the +object, because a `std::weak_ptr` keeps the control block alive: once the +object has been destroyed, the weak pointer stays expired, and the v-table +pointer can never be applied to another object. As for any `virtual_ptr`, the +v-table pointer is invalidated if `initialize` is called again, unless the +registry uses the cpp:indirect_vptr[] policy. + +A `weak_virtual_ptr` converts to a `weak_virtual_ptr` to a base class, but not +to a plain or a shared `virtual_ptr`. A cast to a derived class requires the +object: use `lock`, then `cast`. Since it is not a `virtual_ptr`, a +`weak_virtual_ptr` cannot be used in a virtual parameter, but it can be passed +to a method as an ordinary parameter. Support for `std::weak_ptr` is provided +in ``, which also includes the +`std::shared_ptr` header. diff --git a/doc/modules/ROOT/snippets/policies.cpp b/doc/modules/ROOT/snippets/policies.cpp index 3a92eb4d..eed827a2 100644 --- a/doc/modules/ROOT/snippets/policies.cpp +++ b/doc/modules/ROOT/snippets/policies.cpp @@ -5,7 +5,10 @@ #include #include +#include +#include #include +#include #include #include @@ -118,6 +121,75 @@ BOOST_OPENMETHOD_OVERRIDE( } // namespace fast_perfect_hash_demo +namespace minimal_perfect_hash_demo { + +// tag::minimal_perfect_hash[] +// One slot per type id, whatever the addresses are. Swapping the hash is all it +// takes: `with` replaces the policy of the same category, in place, so +// `vptr_vector` still comes after it. +struct compact_registry : + default_registry::with> {}; +// end::minimal_perfect_hash[] + +BOOST_OPENMETHOD_CLASSES(Animal, Cat, Dog, compact_registry); + +BOOST_OPENMETHOD( + trick, (virtual_ptr), std::string, + compact_registry); + +BOOST_OPENMETHOD_OVERRIDE( + trick, (virtual_ptr), std::string) { + return "spin"; +} + +} // namespace minimal_perfect_hash_demo + +namespace two_level_hash_demo { + +// tag::two_level_hash[] +struct two_level_registry : + default_registry::with> {}; +// end::two_level_hash[] + +BOOST_OPENMETHOD_CLASSES(Animal, Cat, Dog, two_level_registry); + +BOOST_OPENMETHOD( + trick, (virtual_ptr), std::string, + two_level_registry); + +BOOST_OPENMETHOD_OVERRIDE( + trick, (virtual_ptr), std::string) { + return "spin"; +} + +} // namespace two_level_hash_demo + +#if BOOST_OPENMETHOD_HAS_PEXT + +namespace minimal_cover_hash_demo { + +// tag::minimal_cover_hash[] +// Needs BMI2, for every translation unit of the program - hence the guard. +#if BOOST_OPENMETHOD_HAS_PEXT +struct cover_registry : + default_registry::with> {}; +#endif +// end::minimal_cover_hash[] + +BOOST_OPENMETHOD_CLASSES(Animal, Cat, Dog, cover_registry); + +BOOST_OPENMETHOD( + trick, (virtual_ptr), std::string, cover_registry); + +BOOST_OPENMETHOD_OVERRIDE( + trick, (virtual_ptr), std::string) { + return "spin"; +} + +} // namespace minimal_cover_hash_demo + +#endif + namespace stderr_output_demo { // tag::stderr_output[] @@ -226,6 +298,35 @@ BOOST_AUTO_TEST_CASE(rtti_and_storage) { trick(virtual_ptr(snoopy)) == "spin"); } + { + using namespace minimal_perfect_hash_demo; + initialize(); + + Dog snoopy; + BOOST_TEST( + trick(virtual_ptr(snoopy)) == "spin"); + } + + { + using namespace two_level_hash_demo; + initialize(); + + Dog snoopy; + BOOST_TEST( + trick(virtual_ptr(snoopy)) == "spin"); + } + +#if BOOST_OPENMETHOD_HAS_PEXT + { + using namespace minimal_cover_hash_demo; + initialize(); + + Dog snoopy; + BOOST_TEST( + trick(virtual_ptr(snoopy)) == "spin"); + } +#endif + { using namespace stderr_output_demo; initialize(); diff --git a/doc/modules/ROOT/snippets/smart_pointers.cpp b/doc/modules/ROOT/snippets/smart_pointers.cpp index 264a8e41..26291743 100644 --- a/doc/modules/ROOT/snippets/smart_pointers.cpp +++ b/doc/modules/ROOT/snippets/smart_pointers.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #define BOOST_TEST_MODULE openmethod #include @@ -194,3 +195,35 @@ BOOST_AUTO_TEST_CASE(unique_ptr_examples) { BOOST_TEST(cout.str() == "bark\nhiss\n"); } } + +BOOST_AUTO_TEST_CASE(weak_ptr_examples) { + initialize(); + + { + using namespace shared_vptr; + capture_cout cout; + + // tag::weak_lock[] + shared_virtual_ptr animal = make_shared_virtual(); + weak_virtual_ptr observer = animal; + + std::cout << poke(observer.lock()) << "\n"; // bark + + animal = nullptr; + std::cout << std::boolalpha << observer.expired() << "\n"; // true + // end::weak_lock[] + + BOOST_TEST(cout.str() == "bark\ntrue\n"); + } + + { + // tag::weak_pointer[] + shared_virtual_ptr animal = make_shared_virtual(); + weak_virtual_ptr observer = animal; + std::weak_ptr weak = observer.pointer(); + + BOOST_TEST(animal.pointer().use_count() == 1); + BOOST_TEST(weak.lock() == animal.pointer()); + // end::weak_pointer[] + } +} diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index 685d0d57..d6c63daf 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -140,6 +140,9 @@ using macro_default_registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY; template constexpr bool false_t = false; // workaround before CWG2518/P2593R1 +template +struct virtual_ptr_access; + } // namespace detail namespace detail { @@ -1042,6 +1045,26 @@ inline auto unbox_vptr(const vptr_type* vpp) { inline vptr_type null_vptr = nullptr; +// Access to the parts of a `virtual_ptr`, for the classes that carry a +// v-table pointer of their own and exchange it with one: copy it from a +// `virtual_ptr`, hand it back later. The pointer is the boxed one - under +// `indirect_vptr`, the address of the cell that `initialize()` rewrites, which +// the public `vptr()` unboxes away - and constructing with a given v-table +// pointer skips the lookup, which no public constructor does. +template +struct virtual_ptr_access { + using boxed_vptr_type = decltype(VirtualPtr::vp); + + static auto boxed_vptr(const VirtualPtr& ptr) -> boxed_vptr_type { + return ptr.vp; + } + + template + static auto make(Arg&& obj, boxed_vptr_type vp) -> VirtualPtr { + return VirtualPtr(std::forward(obj), vp); + } +}; + } // namespace detail //! Create a `virtual_ptr` for an object of a known exact class. @@ -1188,6 +1211,8 @@ class virtual_ptr { #ifndef __MRDOCS__ template friend class virtual_ptr; + template + friend struct detail::virtual_ptr_access; template friend auto final_virtual_ptr(Arg&& obj); #endif @@ -1546,6 +1571,8 @@ class virtual_ptr< #ifndef __MRDOCS__ template friend class virtual_ptr; + template + friend struct detail::virtual_ptr_access; template friend auto final_virtual_ptr(Arg&& obj); #endif diff --git a/include/boost/openmethod/interop/boost_any.hpp b/include/boost/openmethod/interop/boost_any.hpp index 8a1a85cf..320c32a1 100644 --- a/include/boost/openmethod/interop/boost_any.hpp +++ b/include/boost/openmethod/interop/boost_any.hpp @@ -20,17 +20,18 @@ namespace boost::openmethod { namespace detail { -template -struct validate_method_parameter, Registry, void> : +template +struct validate_method_parameter< + virtual_, Registry, void> : std::true_type {}; -template -struct validate_method_parameter, Registry, void> : - std::true_type {}; +template +struct validate_method_parameter< + virtual_, Registry, void> : std::true_type {}; -template -struct validate_method_parameter, Registry, void> : - std::true_type {}; +template +struct validate_method_parameter< + virtual_, Registry, void> : std::true_type {}; // `boost::any::type()` yields a `std::type_info`, which is a valid `type_id` // only for an rtti policy that identifies classes by `&typeid(T)`. Under any diff --git a/include/boost/openmethod/interop/boost_type_erasure.hpp b/include/boost/openmethod/interop/boost_type_erasure.hpp index fea69760..a6f53f9b 100644 --- a/include/boost/openmethod/interop/boost_type_erasure.hpp +++ b/include/boost/openmethod/interop/boost_type_erasure.hpp @@ -94,35 +94,36 @@ constexpr bool te_pass_through = // placeholder of the parameter, so methods and overriders agree on a // single registered root per Concept. -template +template struct validate_method_parameter< - virtual_&>, Registry, void> : - std::true_type {}; + virtual_&, ParamRegistry>, Registry, + void> : std::true_type {}; -template +template struct validate_method_parameter< - virtual_&>, Registry, void> : + virtual_&, ParamRegistry>, Registry, void> : std::true_type {}; -template +template struct validate_method_parameter< - virtual_&&>, Registry, void> : + virtual_&&, ParamRegistry>, Registry, void> : std::true_type {}; -template +template struct validate_method_parameter< - virtual_>, Registry, void> : + virtual_, ParamRegistry>, Registry, void> : std::true_type {}; -template +template struct validate_method_parameter< - virtual_>, Registry, void> : - std::true_type {}; + virtual_, ParamRegistry>, Registry, + void> : std::true_type {}; -template +template struct validate_method_parameter< - virtual_>, Registry, - void> : std::false_type { + virtual_< + boost::type_erasure::any, ParamRegistry>, + Registry, void> : std::false_type { static_assert( false_t, "an owning type_erasure::any must be passed by reference"); }; diff --git a/include/boost/openmethod/interop/std_any.hpp b/include/boost/openmethod/interop/std_any.hpp index 315268c0..e5ee6247 100644 --- a/include/boost/openmethod/interop/std_any.hpp +++ b/include/boost/openmethod/interop/std_any.hpp @@ -20,17 +20,18 @@ namespace boost::openmethod { namespace detail { -template -struct validate_method_parameter, Registry, void> : +template +struct validate_method_parameter< + virtual_, Registry, void> : std::true_type {}; -template -struct validate_method_parameter, Registry, void> : - std::true_type {}; +template +struct validate_method_parameter< + virtual_, Registry, void> : std::true_type {}; -template -struct validate_method_parameter, Registry, void> : - std::true_type {}; +template +struct validate_method_parameter< + virtual_, Registry, void> : std::true_type {}; // `std::any::type()` yields a `std::type_info`, which is a valid `type_id` // only for an rtti policy that identifies classes by `&typeid(T)`. Under any diff --git a/include/boost/openmethod/interop/std_weak_ptr.hpp b/include/boost/openmethod/interop/std_weak_ptr.hpp new file mode 100644 index 00000000..9e95a499 --- /dev/null +++ b/include/boost/openmethod/interop/std_weak_ptr.hpp @@ -0,0 +1,562 @@ +// 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) + +#ifndef BOOST_OPENMETHOD_INTEROP_WEAK_PTR_HPP +#define BOOST_OPENMETHOD_INTEROP_WEAK_PTR_HPP + +#include +#include +#include + +namespace boost::openmethod { + +template> +class weak_virtual_ptr; + +namespace detail { + +// A weak pointer may refer to an object that no longer exists, so there is +// nothing to dispatch on. `virtual_traits` is deliberately *not* specialized +// for `std::weak_ptr`, and `weak_virtual_ptr` is not a `virtual_ptr`. The +// specializations below only replace the vague diagnostics that would result +// from using either as a virtual parameter with a useful one, in the four +// forms a virtual parameter can take. A `weak_virtual_ptr` that is not wrapped +// in `virtual_` is an ordinary parameter, and needs no specialization. The +// registry `virtual_` carries is a parameter of its own, so that a registry +// spelled on the parameter is diagnosed like the default one. + +template +struct reject_weak_parameter : std::false_type { + static_assert( + false_t, + "a weak pointer cannot be a virtual parameter; call lock() first"); +}; + +template +struct validate_method_parameter< + virtual_, ParamRegistry>, Registry, void> : + reject_weak_parameter {}; + +template +struct validate_method_parameter< + virtual_&, ParamRegistry>, Registry, void> : + reject_weak_parameter {}; + +template +struct validate_method_parameter< + virtual_&, ParamRegistry>, Registry, void> : + reject_weak_parameter {}; + +template +struct validate_method_parameter< + virtual_&&, ParamRegistry>, Registry, void> : + reject_weak_parameter {}; + +template +struct validate_method_parameter< + virtual_, ParamRegistry>, MethodRegistry, + void> : reject_weak_parameter {}; + +template +struct validate_method_parameter< + virtual_&, ParamRegistry>, MethodRegistry, + void> : reject_weak_parameter {}; + +template +struct validate_method_parameter< + virtual_&, ParamRegistry>, + MethodRegistry, void> : reject_weak_parameter {}; + +template +struct validate_method_parameter< + virtual_&&, ParamRegistry>, MethodRegistry, + void> : reject_weak_parameter {}; + +} // namespace detail + +//! Weak pointer to an object, remembering its v-table pointer +//! +//! A `weak_virtual_ptr` tracks an object with a `std::weak_ptr`, and +//! remembers its v-table pointer. It is a storage facility, not a +//! `virtual_ptr`: it cannot be dereferenced, compared, or used as a virtual +//! parameter, because the object may no longer exist. It can be passed to a +//! method as an ordinary parameter. Call `lock()` to obtain a +//! @ref shared_virtual_ptr, then use it as usual. Since the v-table pointer is +//! copied from the weak pointer, `lock()` costs no more than +//! `std::weak_ptr::lock()`: no hash table lookup is needed. +//! +//! Remembering the v-table pointer is safe with respect to the lifetime of the +//! object: a `std::weak_ptr` keeps the control block alive, so once the object +//! is destroyed, the weak pointer stays expired, and the v-table pointer can +//! never be applied to another object. +//! +//! @note As for any `virtual_ptr`, the remembered v-table pointer is +//! invalidated when @ref boost::openmethod::initialize is called again, unless +//! the registry uses @ref policies::indirect_vptr. +//! +//! @par Example +//! include:smart_pointers.cpp#classes;weak_lock +//! +//! @tparam Class The class of the object, possibly cv-qualified +//! @tparam Registry The registry in which `Class` is registered. Defaults to +//! the registry `Class` has an affinity for, see @ref registry_affinity. +//! +//! @see [Smart Pointers](xref:ROOT:smart_pointers.adoc) +template +class weak_virtual_ptr { +#ifndef __MRDOCS__ + template + friend class weak_virtual_ptr; +#endif + + static constexpr bool use_indirect_vptrs = Registry::has_indirect_vptr; + + std::conditional_t vp; + std::weak_ptr obj; + + template + static auto vptr_of(const std::shared_ptr& other) { + return detail::box_vptr( + other ? detail::acquire_vptr(*other) : detail::null_vptr); + } + + template + static auto vptr_of( + const virtual_ptr, Registry>& other) { + return detail::virtual_ptr_access< + virtual_ptr, Registry>>::boxed_vptr(other); + } + + // Lock `other` once: it is needed to find the dynamic type of the object, + // and the `std::weak_ptr` is then constructed from the `std::shared_ptr`, + // which does not lock again, as construction from a `std::weak_ptr` to a + // different class would. An expired `other` is copied as is, which keeps + // its control block - and with it `expired()`, `use_count()` and owner + // identity - as far as the standard library allows: libstdc++ shares + // ownership with a source that is expired but not empty, as + // [util.smartptr.weak.const] requires; libc++ locks first, so an expired + // source of a *different* class yields an empty weak pointer there. + template + void assign(const std::weak_ptr& other) { + auto locked = other.lock(); + vp = vptr_of(locked); + + if (locked) { + obj = locked; + } else { + obj = other; + } + } + + public: + //! Class pointed to by the `std::weak_ptr` + using element_type = Class; + + //! Default constructor + //! + //! Construct an empty `std::weak_ptr`. Set the v-table pointer to + //! `nullptr`. + weak_virtual_ptr() : + vp(detail::box_vptr(detail::null_vptr)) { + } + + //! Construct from `nullptr` + //! + //! Construct an empty `std::weak_ptr`. Set the v-table pointer to + //! `nullptr`. + //! + //! @param value A `nullptr`. + explicit weak_virtual_ptr(std::nullptr_t) : + vp(detail::box_vptr(detail::null_vptr)) { + } + + weak_virtual_ptr(const weak_virtual_ptr& other) = default; + + weak_virtual_ptr(weak_virtual_ptr&& other) noexcept : + vp(std::exchange( + other.vp, detail::box_vptr(detail::null_vptr))), + obj(std::move(other.obj)) { + } + + //! Construct from a `shared_virtual_ptr` to a derived class + //! + //! Copy the v-table pointer from `other`. Construct the `std::weak_ptr` + //! from the `std::shared_ptr` held by `other`. + //! + //! `Other` is _not_ required to be a polymorphic class: the v-table + //! pointer is already known. + //! + //! @par Example + //! include:smart_pointers.cpp#classes;weak_lock + //! + //! @param other A `shared_virtual_ptr` to an object of a class derived from + //! `Class`. + //! + //! @par Requirements + //! @li `std::weak_ptr` must be constructible from + //! `const std::shared_ptr&`. + template< + class Other, + typename = std::enable_if_t, const std::shared_ptr&>>> + weak_virtual_ptr( + const virtual_ptr, Registry>& other) : + vp(vptr_of(other)), obj(other.pointer()) { + } + + //! Construct from a `weak_virtual_ptr` to a derived class + //! + //! Copy the v-table pointer and the `std::weak_ptr` from `other`. + //! + //! @param other A `weak_virtual_ptr` to an object of a class derived from + //! `Class`. + //! + //! @par Requirements + //! @li `std::weak_ptr` must be constructible from + //! `const std::weak_ptr&`. + template< + class Other, + typename = std::enable_if_t, const std::weak_ptr&>>> + weak_virtual_ptr(const weak_virtual_ptr& other) : + vp(other.vp), obj(other.obj) { + } + + //! Move-construct from a `weak_virtual_ptr` to a derived class + //! + //! Copy the v-table pointer from `other`, and set it to `nullptr` in + //! `other`. Move the `std::weak_ptr` from `other`. + //! + //! @param other A `weak_virtual_ptr` to an object of a class derived from + //! `Class`. + //! + //! @par Requirements + //! @li `std::weak_ptr` must be constructible from + //! `std::weak_ptr&&`. + template< + class Other, + typename = std::enable_if_t, std::weak_ptr&&>>> + weak_virtual_ptr(weak_virtual_ptr&& other) noexcept : + vp(std::exchange( + other.vp, detail::box_vptr(detail::null_vptr))), + obj(std::move(other.obj)) { + } + + //! Construct from a `std::shared_ptr` to a derived class + //! + //! Construct the `std::weak_ptr` from `other`. Set the v-table pointer + //! according to the dynamic type of `*other`. + //! + //! @param other A `std::shared_ptr` to a polymorphic object. + //! + //! @par Requirements + //! @li `Other` must be a polymorphic class, according to the `rtti` + //! policy of `Registry`. + //! @li `std::weak_ptr` must be constructible from + //! `const std::shared_ptr&`. + template< + class Other, + typename = std::enable_if_t>, + typename = std::enable_if_t, const std::shared_ptr&>>> + weak_virtual_ptr(const std::shared_ptr& other) : + vp(vptr_of(other)), obj(other) { + } + + //! Construct from a `std::weak_ptr` to a derived class + //! + //! Construct the `std::weak_ptr` from `other`. Lock `other` to find the + //! dynamic type of the object, and set the v-table pointer accordingly. If + //! `other` has expired, the v-table pointer is set to `nullptr`. + //! + //! @param other A `std::weak_ptr` to a polymorphic object. + //! + //! @par Requirements + //! @li `Other` must be a polymorphic class, according to the `rtti` + //! policy of `Registry`. + //! @li `std::weak_ptr` must be constructible from + //! `const std::weak_ptr&`. + template< + class Other, + typename = std::enable_if_t>, + typename = std::enable_if_t, const std::weak_ptr&>>> + weak_virtual_ptr(const std::weak_ptr& other) { + assign(other); + } + + //! Assign from `nullptr` + //! + //! Reset the `std::weak_ptr`. Set the v-table pointer to `nullptr`. + //! + //! @param value A `nullptr`. + weak_virtual_ptr& operator=(std::nullptr_t) noexcept { + reset(); + return *this; + } + + weak_virtual_ptr& operator=(const weak_virtual_ptr& other) = default; + + weak_virtual_ptr& operator=(weak_virtual_ptr&& other) noexcept { + vp = std::exchange( + other.vp, detail::box_vptr(detail::null_vptr)); + obj = std::move(other.obj); + return *this; + } + + //! Assign from a `shared_virtual_ptr` to a derived class + //! + //! Copy the v-table pointer from `other`. Assign the `std::weak_ptr` from + //! the `std::shared_ptr` held by `other`. + //! + //! `Other` is _not_ required to be a polymorphic class: the v-table + //! pointer is already known. + //! + //! @param other A `shared_virtual_ptr` to an object of a class derived from + //! `Class`. + //! + //! @par Requirements + //! @li `std::weak_ptr` must be assignable from + //! `const std::shared_ptr&`. + template< + class Other, + typename = std::enable_if_t&, const std::shared_ptr&>>> + weak_virtual_ptr& operator=( + const virtual_ptr, Registry>& other) { + vp = vptr_of(other); + obj = other.pointer(); + return *this; + } + + //! Assign from a `weak_virtual_ptr` to a derived class + //! + //! Copy the v-table pointer and the `std::weak_ptr` from `other`. + //! + //! @param other A `weak_virtual_ptr` to an object of a class derived from + //! `Class`. + //! + //! @par Requirements + //! @li `std::weak_ptr` must be assignable from + //! `const std::weak_ptr&`. + template< + class Other, + typename = std::enable_if_t&, const std::weak_ptr&>>> + weak_virtual_ptr& operator=( + const weak_virtual_ptr& other) { + vp = other.vp; + obj = other.obj; + return *this; + } + + //! Move-assign from a `weak_virtual_ptr` to a derived class + //! + //! Copy the v-table pointer from `other`, and set it to `nullptr` in + //! `other`. Move the `std::weak_ptr` from `other`. + //! + //! @param other A `weak_virtual_ptr` to an object of a class derived from + //! `Class`. + //! + //! @par Requirements + //! @li `std::weak_ptr` must be assignable from + //! `std::weak_ptr&&`. + template< + class Other, + typename = std::enable_if_t&, std::weak_ptr&&>>> + weak_virtual_ptr& operator=( + weak_virtual_ptr&& other) noexcept { + vp = std::exchange( + other.vp, detail::box_vptr(detail::null_vptr)); + obj = std::move(other.obj); + return *this; + } + + //! Assign from a `std::shared_ptr` to a derived class + //! + //! Assign the `std::weak_ptr` from `other`. Set the v-table pointer + //! according to the dynamic type of `*other`. + //! + //! @param other A `std::shared_ptr` to a polymorphic object. + //! + //! @par Requirements + //! @li `Other` must be a polymorphic class, according to the `rtti` + //! policy of `Registry`. + //! @li `std::weak_ptr` must be assignable from + //! `const std::shared_ptr&`. + template< + class Other, + typename = std::enable_if_t>, + typename = std::enable_if_t&, const std::shared_ptr&>>> + weak_virtual_ptr& operator=(const std::shared_ptr& other) { + vp = vptr_of(other); + obj = other; + return *this; + } + + //! Assign from a `std::weak_ptr` to a derived class + //! + //! Assign the `std::weak_ptr` from `other`. Lock `other` to find the + //! dynamic type of the object, and set the v-table pointer accordingly. If + //! `other` has expired, the v-table pointer is set to `nullptr`. + //! + //! @param other A `std::weak_ptr` to a polymorphic object. + //! + //! @par Requirements + //! @li `Other` must be a polymorphic class, according to the `rtti` + //! policy of `Registry`. + //! @li `std::weak_ptr` must be assignable from + //! `const std::weak_ptr&`. + template< + class Other, + typename = std::enable_if_t>, + typename = std::enable_if_t&, const std::weak_ptr&>>> + weak_virtual_ptr& operator=(const std::weak_ptr& other) { + assign(other); + return *this; + } + + //! Lock the weak pointer + //! + //! Return a `shared_virtual_ptr` to the object, using the remembered + //! v-table pointer. No hash table lookup is performed. + //! + //! @par Example + //! include:smart_pointers.cpp#classes;weak_lock + //! + //! @return A `shared_virtual_ptr` to the object if it still exists, or an + //! empty `shared_virtual_ptr` with a `nullptr` v-table pointer otherwise. + auto lock() const -> virtual_ptr, Registry> { + using shared = virtual_ptr, Registry>; + + if (auto locked = obj.lock()) { + return detail::virtual_ptr_access::make( + std::move(locked), vp); + } + + return shared(); + } + + //! Check whether the object still exists + //! + //! @return `true` if the `std::weak_ptr` is empty or the object has been + //! destroyed, `false` otherwise. + auto expired() const noexcept -> bool { + return obj.expired(); + } + + //! Get the number of `std::shared_ptr` objects sharing the object + //! + //! @return The result of `std::weak_ptr::use_count`. + auto use_count() const noexcept -> long { + return obj.use_count(); + } + + //! Compare owners with a `weak_virtual_ptr` + //! + //! Provide the owner-based ordering that an associative container keyed on + //! `weak_virtual_ptr` needs. Note that `std::owner_less` accepts + //! `std::shared_ptr` and `std::weak_ptr` alone in some implementations, so + //! the comparator is best written as a function object calling + //! `owner_before`. + //! + //! @param other A `weak_virtual_ptr`. + //! + //! @return The result of `std::weak_ptr::owner_before` applied to the + //! `std::weak_ptr` held by `other`. + template + auto owner_before( + const weak_virtual_ptr& other) const noexcept -> bool { + return obj.owner_before(other.obj); + } + + //! Compare owners with a `shared_virtual_ptr` + //! + //! @param other A `shared_virtual_ptr`. + //! + //! @return The result of `std::weak_ptr::owner_before` applied to the + //! `std::shared_ptr` held by `other`. + template + auto owner_before( + const virtual_ptr, Registry>& other) + const noexcept -> bool { + return obj.owner_before(other.pointer()); + } + + //! Release the reference to the object + //! + //! Reset the `std::weak_ptr`. Set the v-table pointer to `nullptr`. + void reset() noexcept { + obj.reset(); + vp = detail::box_vptr(detail::null_vptr); + } + + //! Swap with another `weak_virtual_ptr` + //! + //! @param other A `weak_virtual_ptr` to the same class. + void swap(weak_virtual_ptr& other) noexcept { + std::swap(vp, other.vp); + obj.swap(other.obj); + } + + //! Get the weak pointer to the object + //! + //! @par Example + //! include:smart_pointers.cpp#classes;weak_pointer + //! + //! @return A const reference to the `std::weak_ptr` + auto pointer() const noexcept -> const std::weak_ptr& { + return obj; + } + + //! Get the v-table pointer + //! + //! @return A pointer to the v-table remembered when the `weak_virtual_ptr` + //! was created or assigned, or `nullptr`. + auto vptr() const { + return detail::unbox_vptr(this->vp); + } +}; + +//! Reject a `virtual_ptr` to a `std::weak_ptr` +//! +//! A `std::weak_ptr` may refer to an object that no longer exists, so a +//! `virtual_ptr` cannot track an object through one. This specialization +//! rejects the combination at compile time, which also covers +//! @ref final_virtual_ptr, since that instantiates the `virtual_ptr` it +//! returns. Use @ref weak_virtual_ptr instead. +//! +//! The specialization steps aside if `virtual_traits` is specialized for +//! `std::weak_ptr`. +//! +//! @tparam Class The class pointed to by the `std::weak_ptr`. +//! @tparam Registry A @ref registry. +template +class virtual_ptr< + std::weak_ptr, Registry, + std::enable_if_t< + BOOST_OPENMETHOD_UNLESS_MRDOCS(detail::) + IsSmartPtr, Registry> == false>> { + static_assert( + detail::false_t, + "a std::weak_ptr cannot be wrapped in a virtual_ptr; use " + "weak_virtual_ptr"); +}; + +namespace aliases { +using boost::openmethod::weak_virtual_ptr; +} // namespace aliases + +} // namespace boost::openmethod + +#endif diff --git a/include/boost/openmethod/macros.hpp b/include/boost/openmethod/macros.hpp index 4e2401be..32db7c32 100644 --- a/include/boost/openmethod/macros.hpp +++ b/include/boost/openmethod/macros.hpp @@ -73,18 +73,30 @@ inline constexpr bool method_not_found = false; //! Create a registrar object. //! -//! Creates a registrar for a type, i.e. a static object of that type with a -//! unique generated name. At static initialization time, the object adds -//! itself to a list: methods and class registrations add themselves to a +//! Creates a registrar for a type, i.e. an object of that type with a unique +//! generated name. At static initialization time, the object adds itself to a +//! list: methods and class registrations add themselves to a //! @ref boost::openmethod::registry, and overriders add themselves to a //! method's overrider list. //! +//! The registrar is declared `inline`, so this macro can be used inside a +//! class body - the object then becomes a `static` data member, with the +//! same access to the class's private members as any other member. That is +//! what lets an overrider be a `static` member function of the class it +//! needs access to: see @ref BOOST_OPENMETHOD_OVERRIDE_FN. +//! +//! @note `inline` is illegal on a variable declared at block (function) +//! scope. Code that calls this macro inside a function body, to control +//! exactly when registration happens relative to +//! @ref boost::openmethod::initialize, must spell out the macro's expansion +//! by hand instead: `static TYPE BOOST_OPENMETHOD_GENSYM;` (no `inline`). +//! //! @param ... The registrar's type. It is variadic so that it may contain //! unparenthesized commas, as in `std::pair`. //! //! @see [Core API](xref:ROOT:core_api.adoc) #define BOOST_OPENMETHOD_REGISTER(...) \ - static __VA_ARGS__ BOOST_OPENMETHOD_GENSYM + static inline __VA_ARGS__ BOOST_OPENMETHOD_GENSYM //! Generate a method id. //! @@ -583,6 +595,46 @@ inline constexpr bool method_not_found = false; ID, PARAMETERS, __VA_ARGS__)::fn PARAMETERS \ -> boost::mp11::mp_back> +//! Add one or more existing functions to a method as overriders. +//! +//! Unlike @ref BOOST_OPENMETHOD_OVERRIDE, which declares and defines a new +//! overrider, `BOOST_OPENMETHOD_OVERRIDE_FN` registers functions that already +//! exist - free functions, or `static` member functions of a class. +//! +//! `ID`, `PARAMETERS` and the return type are the method's own, exactly as +//! given to @ref BOOST_OPENMETHOD; they are not any individual overrider's. +//! Each function in `...` is still checked against them the same way the +//! overrider of @ref BOOST_OPENMETHOD_OVERRIDE is: same arity, +//! `virtual_ptr` and `virtual_` parameters covariant with the method's, +//! other parameters identical, return type the same or covariant. +//! +//! Because it expands through @ref BOOST_OPENMETHOD_REGISTER, this macro can +//! be used inside a class body, registering one or more `static` member +//! functions of that class as overriders - which, being members, have the +//! same access to the class's private state as any other member, with no +//! need to `friend` anything. See [Friends](xref:ROOT:friends.adoc) for the +//! `friend`-based alternative this replaces when the class is under the +//! caller's control. +//! +//! @note `ID` must be an *identifier*. Qualified names are not allowed. +//! +//! @note The return type is a single macro argument, unlike the trailing +//! `...` of @ref BOOST_OPENMETHOD_TYPE; it does not accept an explicit +//! registry argument after it in the same call. A method that +//! overrides a non-default registry can still be targeted by spelling the +//! registration out: `BOOST_OPENMETHOD_REGISTER(BOOST_OPENMETHOD_TYPE(ID, +//! PARAMETERS, RETURN, REGISTRY)::override)`. +//! +//! @param ID The method's name. +//! @param PARAMETERS The method's parameter list, in parentheses. +//! @param RETURN The method's return type. +//! @param ... One or more functions to add as overriders. +//! +//! @see [Friends](xref:ROOT:friends.adoc) +#define BOOST_OPENMETHOD_OVERRIDE_FN(ID, PARAMETERS, RETURN, ...) \ + BOOST_OPENMETHOD_REGISTER( \ + BOOST_OPENMETHOD_TYPE(ID, PARAMETERS, RETURN)::override<__VA_ARGS__>) + //! Register classes. //! //! Registers classes in a registry. diff --git a/include/boost/openmethod/policies/fast_perfect_hash.hpp b/include/boost/openmethod/policies/fast_perfect_hash.hpp index 3abd355f..5d8b4387 100644 --- a/include/boost/openmethod/policies/fast_perfect_hash.hpp +++ b/include/boost/openmethod/policies/fast_perfect_hash.hpp @@ -13,25 +13,22 @@ #include #include #include + #ifdef _MSC_VER #pragma warning(push) -#pragma warning(disable : 4702) // unreachable code +// 4702: unreachable code. The `abort()` after a call to the error handler is +// there for a handler that returns - the default one prints and returns - but a +// handler that is [[noreturn]], like throw_error_handler, makes it dead code, +// and MSVC diagnoses that. Same reason as in preamble.hpp and core.hpp. +#pragma warning(disable : 4702) #endif namespace boost::openmethod { namespace detail { -#if defined(UINTPTR_MAX) -using uintptr = std::uintptr_t; -constexpr uintptr uintptr_max = UINTPTR_MAX; -#else -static_assert( - sizeof(std::size_t) == sizeof(void*), - "This implementation requires that size_t and void* have the same size."); -using uintptr = std::size_t; -constexpr uintptr uintptr_max = (std::numeric_limits::max)(); -#endif +// detail::uintptr and detail::uintptr_max are in preamble.hpp: every +// `type_hash` policy needs them, not just this one. struct hash_fn { std::size_t mult; @@ -320,4 +317,8 @@ auto fast_perfect_hash::search_error::write(Stream& os) const -> void { } // namespace policies } // namespace boost::openmethod +#ifdef _MSC_VER +#pragma warning(pop) +#endif + #endif diff --git a/include/boost/openmethod/policies/minimal_cover_hash.hpp b/include/boost/openmethod/policies/minimal_cover_hash.hpp new file mode 100644 index 00000000..f11366aa --- /dev/null +++ b/include/boost/openmethod/policies/minimal_cover_hash.hpp @@ -0,0 +1,531 @@ +// 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) + +#ifndef BOOST_OPENMETHOD_POLICY_MINIMAL_COVER_HASH_HPP +#define BOOST_OPENMETHOD_POLICY_MINIMAL_COVER_HASH_HPP + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +// Detect BMI2's parallel bit extract. GCC and clang define __BMI2__ when the +// instruction is enabled, which takes -mbmi2 or a -march= that implies it, and +// reject the intrinsic without it. MSVC gates nothing on a macro and emits the +// instruction from the intrinsic, so there the test is the target alone. +// +// clang-cl answers to both descriptions, and must be read as clang: it defines +// _MSC_VER and _M_X64, but `_pext_u64` is still the always_inline function that +// needs the `bmi2` target feature. Taking it for MSVC would turn this macro on +// for a compiler that then refuses the intrinsic - "always_inline function +// '_pext_u64' requires target feature 'bmi2'" - which is the error this macro +// exists to keep users away from. Hence the !defined(__clang__): clang-cl falls +// to the first arm, where it belongs, and it defines __x86_64__ as well. +// +// Either way the target must be x86-*64*. `_pext_u64` extracts from a 64-bit +// value and exists only in 64-bit mode: on 32-bit x86 there is `_pext_u32` and +// nothing wider, so a guard that accepted `_M_IX86` - or `__BMI2__` on an +// `-m32` build - would let the header reach an intrinsic that is not declared. +// +// The detection and the documented macro are separate so that the latter is one +// unconditional #define, with its doc comment directly attached. A comment +// separated from its #define by a preprocessor directive is not attached to it, +// and MrDocs then produces no page - which would make every @ref to the macro +// render as plain text. +#if (defined(__BMI2__) && defined(__x86_64__)) || \ + (defined(_MSC_VER) && !defined(__clang__) && defined(_M_X64)) +#define BOOST_OPENMETHOD_DETAIL_HAS_PEXT 1 +#else +#define BOOST_OPENMETHOD_DETAIL_HAS_PEXT 0 +#endif + +//! Whether @ref boost::openmethod::policies::minimal_cover_hash can be used on +//! this target. +//! +//! 1 if the compiler can emit BMI2's parallel bit extract, `pext`, and 0 +//! otherwise. The header always compiles; what fails, with a diagnostic, is +//! naming the policy in a registry when this is 0. A program that offers the +//! policy as an option guards the declaration with it: +//! +//! @code +//! #if BOOST_OPENMETHOD_HAS_PEXT +//! struct my_registry : +//! boost::openmethod::default_registry::with< +//! boost::openmethod::policies::minimal_cover_hash<>> {}; +//! #endif +//! @endcode +//! +//! @see [Registries and Policies](xref:ROOT:registries_and_policies.adoc) +#define BOOST_OPENMETHOD_HAS_PEXT BOOST_OPENMETHOD_DETAIL_HAS_PEXT + +#if BOOST_OPENMETHOD_HAS_PEXT +#include +#endif + +#ifdef _MSC_VER +#pragma warning(push) +// 4702: unreachable code. The `abort()` after a call to the error handler is +// there for a handler that returns - the default one prints and returns - but a +// handler that is [[noreturn]], like throw_error_handler, makes it dead code, +// and MSVC diagnoses that. Same reason as in preamble.hpp and core.hpp. +#pragma warning(disable : 4702) +#endif + +namespace boost::openmethod { + +namespace detail { + +// BOOST_OPENMETHOD_HAS_PEXT, made dependent on a template parameter. +// +// A static_assert whose condition does not depend on the enclosing template may +// be diagnosed as soon as the template is *defined*, rather than when it is +// instantiated - the standard calls such a template ill-formed, no diagnostic +// required, and compilers differ on when they report it. GCC 11 and 12, and +// Clang 13 through 15, report it immediately; GCC 13 and later, and Clang 18 +// and later, wait for the instantiation. Written the obvious way, the assertion +// in minimal_cover_hash::fn would therefore make *including this header* an +// error on those compilers whenever the instruction is unavailable - which is +// the one thing the header promises not to do. +template +inline constexpr bool has_pext = BOOST_OPENMETHOD_HAS_PEXT != 0; + +// Cold path only: the cover search counts mask bits, the dispatch path does +// not. Plain C++ rather than an intrinsic, so it carries no instruction-set +// requirement of its own - minimal_cover_hash already has one, and one is +// quite enough. +inline auto popcount64(std::uint64_t bits) -> std::size_t { +#if defined(__GNUC__) || defined(__clang__) + return std::size_t(__builtin_popcountll(bits)); +#else + bits = bits - ((bits >> 1) & 0x5555555555555555ull); + bits = + (bits & 0x3333333333333333ull) + ((bits >> 2) & 0x3333333333333333ull); + bits = (bits + (bits >> 4)) & 0x0f0f0f0f0f0f0f0full; + + return std::size_t((bits * 0x0101010101010101ull) >> 56); +#endif +} + +// The dispatch path's one instruction. Wrapped so that the header parses on a +// target without it: the stub is never reached, because naming the policy in a +// registry static_asserts first. +inline auto pext64(std::uint64_t value, std::uint64_t mask) -> std::uint64_t { +#if BOOST_OPENMETHOD_HAS_PEXT + return _pext_u64(value, mask); +#else + (void)value; + (void)mask; + + return 0; +#endif +} + +} // namespace detail + +namespace policies { + +//! Map type ids to indexes by extracting a minimal cover of their bits. +//! +//! `minimal_cover_hash` implements the @ref type_hash policy as +//! `H(x) = pext(x, mask)`: the bits of `x` selected by `mask`, packed into the +//! low `popcount(mask)` positions by BMI2's parallel bit extract, one +//! instruction. The index range is `[0, 2^popcount(mask))`. +//! +//! `mask` is a *minimal cover*: a smallest-found set of bit positions such that +//! `x & mask` is still injective over the registered type ids. That is exactly +//! the condition for `pext` to be injective, so the search never needs `pext` +//! itself. Unlike @ref fast_perfect_hash's randomized multiplier search it is +//! deterministic, and it finishes in milliseconds on inputs where that search +//! gives up: +//! +//! @li the bits that vary at all are trivially a cover; +//! @li a greedy pass drops bits, lowest entropy first, while injectivity holds; +//! @li a second greedy pass builds a cover bottom-up, adding the bit that +//! resolves the most collisions each time, then trims it the same way; +//! @li the smaller of the two wins. +//! +//! **Choose it when dispatch must not get slower but the default search is a +//! problem.** One `pext` costs about what a multiply and a shift cost, so +//! dispatch is as fast as with @ref fast_perfect_hash; what this buys is a +//! table found deterministically, in bounded time. Its footprint is comparable +//! to `fast_perfect_hash`{empty}'s - both widen when the type ids are sparse, +//! and for the same reason - so it is not the policy to pick for memory. +//! @ref minimal_perfect_hash is. +//! +//! Type ids from different modules differ in many high bits, but those bits are +//! perfectly correlated, since they all encode which module. The cover keeps +//! about `log2(modules)` of them and the rest cost nothing, so a program that +//! `dlopen`{empty}s modules pays one extra bit rather than an unusable table. +//! +//! @warning **BMI2 is required, and that is not a portable requirement.** `pext` +//! requires a 64-bit x86 target - it is absent on ARM and on 32-bit x86 +//! altogether, absent on x86-64 before Haswell and Excavator, and is microcoded +//! on AMD Zen 1 and Zen 2 - around 18 cycles rather than 3 - where this policy +//! will be slower than the default rather than faster. Because `hash` is +//! inlined into every dispatch, `-mbmi2` (or a `-march=` implying it) has to be +//! set for **every** translation unit of the program, and of any module sharing +//! the registry, not just one; a binary built with it executes an illegal +//! instruction on the first dispatch on a CPU that lacks `pext`. MSVC is the +//! exception: it emits the instruction from the intrinsic on any 64-bit target, +//! and takes no flag; clang-cl is not MSVC here, and wants `-mbmi2` or +//! `/arch:AVX2`. Naming this policy in a registry where +//! @ref BOOST_OPENMETHOD_HAS_PEXT is 0 is a compile error. +//! @ref minimal_perfect_hash is the portable alternative, at a cost of a +//! nanosecond or two per call. +//! +//! After "Perfect Hashing in an Imperfect World", Joaquin M. Lopez Munoz. +//! +//! @tparam MaxBits Refuse a cover wider than this; the table is `8 << bits` +//! bytes. +//! +//! @par Example +//! include:policies.cpp#minimal_cover_hash +//! +//! @see [Registries and Policies](xref:ROOT:registries_and_policies.adoc) +template +struct minimal_cover_hash : type_hash { + + //! The minimal cover found is too wide for a table. + struct too_many_bits : openmethod_error { + //! Number of registered type ids. + std::size_t classes; + //! Width of the smallest cover found. + std::size_t bits; + + template + auto write(Stream& os) const -> void; + }; + + using errors = std::variant; + + //! `state` layout when runtime checks are disabled. + struct no_checks { + //! The bit positions to extract. + std::uint64_t mask; + //! The highest index in use. + std::size_t max_value; + }; + + //! `state` layout when runtime checks are enabled: adds the table of + //! registered type ids used to validate hashed types. + struct with_checks : no_checks { + std::vector control; + }; + + //! A TypeHashFn metafunction. + //! + //! @tparam Registry The registry containing this policy + template + class fn { + static_assert( + detail::has_pext, + "minimal_cover_hash needs BMI2: compile every translation unit " + "with -mbmi2 (or a -march= that implies it), or use " + "minimal_perfect_hash, which is portable."); + + public: + using state = std::conditional_t< + Registry::has_runtime_checks, with_checks, no_checks>; + + private: + static auto& st() { + return Registry::template state>(); + } + + static void check(std::size_t index, type_id type); + + static auto injective( + const std::vector& ids, std::uint64_t mask, + std::vector& scratch) -> bool; + static auto collisions( + const std::vector& ids, std::uint64_t mask, + std::vector& scratch) -> std::size_t; + static auto minimal_cover(const std::vector& ids) + -> std::uint64_t; + + public: + //! Finds the cover. + //! + //! @tparam Context An @ref InitializeContext. + //! @param ctx A Context object. + //! @param options A tuple of option objects. + template + static auto initialize( + const Context& ctx, const std::tuple& options) -> void; + + //! Returns the hash range: `[0, max index in use]`. + static auto hash_range() -> std::pair { + return std::pair{std::size_t(0), st().max_value}; + } + + //! Map a type id to an index + //! + //! @param type The type_id to map + //! @return The index + BOOST_FORCEINLINE + static auto hash(type_id type) -> std::size_t { + auto index = std::size_t( + detail::pext64( + static_cast( + reinterpret_cast(type)), + st().mask)); + + if constexpr (Registry::has_runtime_checks) { + check(index, type); + } + + return index; + } + + //! Releases the control table, if there is one. + template + static auto finalize(const std::tuple& options) -> void { + (void)options; + + st().mask = 0; + st().max_value = 0; + + if constexpr (Registry::has_runtime_checks) { + st().control.clear(); + st().control.shrink_to_fit(); + } + } + }; +}; + +template +template +auto minimal_cover_hash::fn::injective( + const std::vector& ids, std::uint64_t mask, + std::vector& scratch) -> bool { + scratch.clear(); + + for (auto id : ids) { + scratch.push_back(id & mask); + } + + std::sort(scratch.begin(), scratch.end()); + + return std::adjacent_find(scratch.begin(), scratch.end()) == scratch.end(); +} + +template +template +auto minimal_cover_hash::fn::collisions( + const std::vector& ids, std::uint64_t mask, + std::vector& scratch) -> std::size_t { + scratch.clear(); + + for (auto id : ids) { + scratch.push_back(id & mask); + } + + std::sort(scratch.begin(), scratch.end()); + std::size_t count = 0; + + for (std::size_t i = 1; i < scratch.size(); ++i) { + count += scratch[i] == scratch[i - 1]; + } + + return count; +} + +template +template +auto minimal_cover_hash::fn::minimal_cover( + const std::vector& ids) -> std::uint64_t { + // The bits that vary at all: trivially a cover, since two distinct ids + // differ in at least one of them. + std::uint64_t universe = 0; + + for (auto id : ids) { + universe |= id ^ ids.front(); + } + + // Entropy of each varying bit: a bit almost every id agrees on separates + // few pairs, so it is the first candidate for dropping. + struct bit_info { + int bit; + double entropy; + }; + + std::vector bits; + + for (int bit = 0; bit < 64; ++bit) { + if (!((universe >> bit) & 1)) { + continue; + } + + std::size_t ones = 0; + + for (auto id : ids) { + ones += (id >> bit) & 1; + } + + auto p = double(ones) / double(ids.size()); + auto entropy = (p <= 0.0 || p >= 1.0) + ? 0.0 + : -(p * std::log2(p) + (1 - p) * std::log2(1 - p)); + bits.push_back({bit, entropy}); + } + + std::stable_sort( + bits.begin(), bits.end(), [](const bit_info& a, const bit_info& b) { + return a.entropy < b.entropy; + }); + + std::vector scratch; + scratch.reserve(ids.size()); + + auto drop = [&](std::uint64_t mask) { + for (const auto& info : bits) { + auto candidate = mask & ~(std::uint64_t(1) << info.bit); + + if (candidate != mask && injective(ids, candidate, scratch)) { + mask = candidate; + } + } + + return mask; + }; + + auto by_drop = drop(universe); + + // Bottom-up: add the bit that resolves the most collisions. + std::uint64_t by_add = 0; + + while (!injective(ids, by_add, scratch)) { + int best_bit = -1; + std::size_t best_count = (std::numeric_limits::max)(); + + for (const auto& info : bits) { + auto bit = std::uint64_t(1) << info.bit; + + if (by_add & bit) { + continue; + } + + auto count = collisions(ids, by_add | bit, scratch); + + if (count < best_count) { + best_count = count; + best_bit = info.bit; + } + } + + by_add |= std::uint64_t(1) << best_bit; + } + + by_add = drop(by_add); + + return detail::popcount64(by_add) < detail::popcount64(by_drop) ? by_add + : by_drop; +} + +template +template +template +auto minimal_cover_hash::fn::initialize( + const Context& ctx, const std::tuple& options) -> void { + (void)options; + + std::vector ids; + + for (auto iter = ctx.classes_begin(); iter != ctx.classes_end(); ++iter) { + for (auto type_iter = iter->type_id_begin(); + type_iter != iter->type_id_end(); ++type_iter) { + ids.push_back( + static_cast( + reinterpret_cast(*type_iter))); + } + } + + // One class may be registered under the same type id by several modules; + // the cover is over *distinct* ids. + std::sort(ids.begin(), ids.end()); + ids.erase(std::unique(ids.begin(), ids.end()), ids.end()); + + if (ids.empty()) { + st().mask = 0; + st().max_value = 0; + + return; + } + + auto mask = ids.size() == 1 ? 0 : minimal_cover(ids); + auto bits = detail::popcount64(mask); + + if (bits > MaxBits) { + too_many_bits error; + error.classes = ids.size(); + error.bits = bits; + + if constexpr (Registry::has_error_handler) { + Registry::error_handler::error(error); + } + + abort(); + } + + st().mask = mask; + st().max_value = 0; + + for (auto id : ids) { + st().max_value = + (std::max)(st().max_value, std::size_t(detail::pext64(id, mask))); + } + + if constexpr (Context::template has_option) { + ctx.tr << " type ids: " << ids.size() << ", cover: " << bits + << " bits, table: " << (st().max_value + 1) << " slots\n"; + } + + if constexpr (Registry::has_runtime_checks) { + st().control.assign(st().max_value + 1, type_id(detail::uintptr_max)); + + for (auto id : ids) { + st().control[std::size_t(detail::pext64(id, mask))] = + reinterpret_cast(id); + } + } +} + +template +template +void minimal_cover_hash::fn::check( + std::size_t index, type_id type) { + if (index > st().max_value || st().control[index] != type) { + if constexpr (Registry::has_error_handler) { + missing_class error; + error.type = type; + Registry::error_handler::error(error); + } + + abort(); + } +} + +template +template +auto minimal_cover_hash::too_many_bits::write(Stream& os) const + -> void { + os << "the smallest bit cover of " << classes << " type ids is " << bits + << " bits wide, more than the " << MaxBits << " allowed\n"; +} + +} // namespace policies +} // namespace boost::openmethod + +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +#endif diff --git a/include/boost/openmethod/policies/minimal_perfect_hash.hpp b/include/boost/openmethod/policies/minimal_perfect_hash.hpp new file mode 100644 index 00000000..b9096928 --- /dev/null +++ b/include/boost/openmethod/policies/minimal_perfect_hash.hpp @@ -0,0 +1,497 @@ +// 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) + +#ifndef BOOST_OPENMETHOD_POLICY_MINIMAL_PERFECT_HASH_HPP +#define BOOST_OPENMETHOD_POLICY_MINIMAL_PERFECT_HASH_HPP + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#if defined(_MSC_VER) +#include +#endif + +#ifdef _MSC_VER +#pragma warning(push) +// 4702: unreachable code. The `abort()` after a call to the error handler is +// there for a handler that returns - the default one prints and returns - but a +// handler that is [[noreturn]], like throw_error_handler, makes it dead code, +// and MSVC diagnoses that. Same reason as in preamble.hpp and core.hpp. +#pragma warning(disable : 4702) +#endif + +namespace boost::openmethod::policies { + +//! Map type ids to a dense index with a minimal perfect hash. +//! +//! `minimal_perfect_hash` implements the @ref type_hash policy by hash and +//! displace, after Belazzougui, Botelho and Dietzfelbinger, without the +//! compression step: +//! +//! @code +//! h = x * seed; // one multiply +//! p = pilots[h >> bucket_shift]; // this bucket's pilot +//! index = mulhi(h * p, slots); // displace, then reduce +//! @endcode +//! +//! The type ids are split into `n / Lambda` buckets by the top bits of `h`. +//! Buckets are placed largest first; each is given a 32-bit odd *pilot*, found +//! by trial, such that multiplying the key by it sends every id in the bucket +//! to a slot that is still free. The final reduction is a multiply-shift - the +//! top half of a 64x64 product - which maps onto `[0, slots)` for any `slots` +//! without a division. +//! +//! **Choose it when the table size matters more than the last nanosecond.** +//! Unlike @ref fast_perfect_hash, whose table is sized by the *distribution* +//! of the type ids and whose randomized search can fail outright on a large, +//! sparse set, this one spends `8 * n * 100 / LoadPercent` bytes of v-table +//! vector plus `4 * n / Lambda` bytes of pilots **whatever the addresses are**, +//! and its search time depends only on how many type ids there are, not where +//! they sit. That makes it the policy to reach for in a program that `dlopen`s +//! modules registering classes of their own, where type ids from different +//! modules are far apart and in unrelated ranges. +//! +//! The price is on the dispatch path: the pilot must be loaded before the index +//! can be formed, so the v-table lookup becomes two dependent loads instead of +//! one. Expect it to cost a nanosecond or two per call relative to +//! @ref fast_perfect_hash. +//! +//! It needs no instruction-set extension - a 64-bit multiply and a shift exist +//! everywhere - and makes no assumption about the layout of the type ids, so +//! unlike @ref minimal_cover_hash it is available on every target, and unlike +//! a scheme keyed on address arithmetic it does not depend on +//! @ref std_rtti. +//! +//! @note **A type id of zero is outside this policy's domain.** Zero is a fixed +//! point of a multiply, so it lands in slot 0 for every seed and every pilot; +//! it cannot be displaced, and the search fails whenever another bucket has +//! taken that slot. Addresses are never zero, so @ref std_rtti and +//! @ref static_rtti are unaffected; a custom @ref rtti policy that hands out +//! small integers must not use zero as one of them. When the registry has +//! @ref runtime_checks, @ref initialize asserts that none of the registered +//! type ids is zero. +//! +//! `LoadPercent = 100` asks for an exactly minimal table. It is reachable, but +//! not in bounded time at a large `Lambda`: the last buckets have to hit the +//! last few free slots, and the expected number of trials for a bucket of size +//! `s` facing a fraction `phi` of free slots grows as `phi^-s`. A few percent +//! of slack removes that tail. Note also that an exactly minimal table is not +//! the smallest *total*: reaching it needs the buckets halved, which doubles +//! the pilot array, and that costs more than the slots it recovers. +//! +//! @tparam Lambda Average bucket size. Larger means a smaller pilot table and +//! a longer search. +//! @tparam LoadPercent Slots per 100 type ids, inverted: 100 is an exactly +//! minimal table, 95 leaves one slot free in twenty. +//! @tparam MaxSeeds How many multipliers to try before reporting +//! @ref search_error. Raising it rarely helps on its own - a set that +//! defeats one multiplier usually defeats them all at that `Lambda` and +//! `LoadPercent`; lower `Lambda` or `LoadPercent` instead. +//! +//! @par Example +//! include:policies.cpp#minimal_perfect_hash +//! +//! @see [Registries and Policies](xref:ROOT:registries_and_policies.adoc) +template< + std::size_t Lambda = 4, std::size_t LoadPercent = 95, + std::size_t MaxSeeds = 16> +struct minimal_perfect_hash : type_hash { + + //! No seed yielded a complete assignment. + struct search_error : openmethod_error { + //! Number of registered type ids. + std::size_t classes; + //! Number of multipliers tried. + std::size_t seeds; + + template + auto write(Stream& os) const -> void; + }; + + using errors = std::variant; + + static_assert(Lambda > 0); + static_assert(LoadPercent > 0 && LoadPercent <= 100); + + //! `state` layout when runtime checks are disabled. + struct no_checks { + //! The multiplier. + std::uint64_t seed; + //! `64 - log2(buckets)`. + std::size_t bucket_shift; + //! The number of slots. + std::uint64_t size; + //! One 32-bit pilot per bucket. + std::vector pilots; + }; + + //! `state` layout when runtime checks are enabled: adds the table of + //! registered type ids used to validate hashed types. + struct with_checks : no_checks { + std::vector control; + }; + + //! A TypeHashFn metafunction. + //! + //! @tparam Registry The registry containing this policy + template + class fn { + public: + using state = std::conditional_t< + Registry::has_runtime_checks, with_checks, no_checks>; + + private: + static auto& st() { + return Registry::template state< + minimal_perfect_hash>(); + } + + static void check(std::size_t index, type_id type); + + // The pilot tried at step `k`. Multiplying by an odd constant is a + // bijection on 32 bits, so the sequence walks the whole range; the + // low bit is set because an even pilot loses the key's high bits. + static auto pilot_at(std::uint32_t k) -> std::uint32_t { + return (k * 0x9e3779b9u) | 1u; + } + + // The top half of a 64x64 product. + static auto mulhi(std::uint64_t a, std::uint64_t b) -> std::uint64_t { +#if defined(__SIZEOF_INT128__) + return std::uint64_t((static_cast<__uint128_t>(a) * b) >> 64); +#elif defined(_MSC_VER) && defined(_M_X64) + return __umulh(a, b); +#else + auto lo = [](std::uint64_t v) { return v & 0xffffffffull; }; + auto hi = [](std::uint64_t v) { return v >> 32; }; + auto ll = lo(a) * lo(b); + auto lh = lo(a) * hi(b); + auto hl = hi(a) * lo(b); + auto hh = hi(a) * hi(b); + auto mid = hi(ll) + lo(lh) + lo(hl); + + return hh + hi(lh) + hi(hl) + hi(mid); +#endif + } + + // Where a pilot sends a key. + static auto place( + std::uint64_t h, std::uint32_t pilot, std::uint64_t slots) + -> std::size_t { + return std::size_t(mulhi(h * pilot, slots)); + } + + static auto mix_seed(std::uint64_t z) -> std::uint64_t { + z += 0x9e3779b97f4a7c15ull; + z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9ull; + z = (z ^ (z >> 27)) * 0x94d049bb133111ebull; + + return (z ^ (z >> 31)) | 1; + } + + static auto build( + const std::vector& ids, std::uint64_t seed, + std::size_t bucket_shift, std::uint64_t slots, + std::vector& pilots) -> bool; + + public: + //! Finds a seed and the pilots. + //! + //! @tparam Context An @ref InitializeContext. + //! @param ctx A Context object. + //! @param options A tuple of option objects. + template + static auto initialize( + const Context& ctx, const std::tuple& options) -> void; + + //! Returns the hash range: `[0, slots - 1]`. + static auto hash_range() -> std::pair { + return std::pair{ + std::size_t(0), std::size_t(st().size ? st().size - 1 : 0)}; + } + + //! Map a type id to an index + //! + //! @param type The type_id to map + //! @return The index + BOOST_FORCEINLINE + static auto hash(type_id type) -> std::size_t { + auto h = std::uint64_t(reinterpret_cast(type)) * + st().seed; + auto pilot = st().pilots[std::size_t(h >> st().bucket_shift)]; + auto index = place(h, pilot, st().size); + + if constexpr (Registry::has_runtime_checks) { + check(index, type); + } + + return index; + } + + //! Releases the pilot table, and the control table if there is one. + template + static auto finalize(const std::tuple& options) -> void { + (void)options; + + st().pilots.clear(); + st().pilots.shrink_to_fit(); + st().size = 0; + + if constexpr (Registry::has_runtime_checks) { + st().control.clear(); + st().control.shrink_to_fit(); + } + } + }; +}; + +template +template +auto minimal_perfect_hash::fn::build( + const std::vector& ids, std::uint64_t seed, + std::size_t bucket_shift, std::uint64_t slots, + std::vector& pilots) -> bool { + auto n = ids.size(); + auto buckets = pilots.size(); + + // Bucket each id, and keep the hashed key the pilot will displace. + std::vector keys(n); + std::vector bucket_of(n); + + for (std::size_t i = 0; i != n; ++i) { + auto h = ids[i] * seed; + bucket_of[i] = std::uint32_t(h >> bucket_shift); + keys[i] = h; + } + + // Group by bucket: counting sort into a CSR-style pair of arrays. + std::vector start(buckets + 1, 0); + + for (auto b : bucket_of) { + ++start[b + 1]; + } + + std::partial_sum(start.begin(), start.end(), start.begin()); + std::vector members(n); + auto fill = start; + + for (std::size_t i = 0; i != n; ++i) { + members[fill[bucket_of[i]]++] = std::uint32_t(i); + } + + // Largest buckets first: a big bucket is placeable only while the table + // is still mostly empty. + std::vector order(buckets); + std::iota(order.begin(), order.end(), std::uint32_t(0)); + std::stable_sort( + order.begin(), order.end(), [&](std::uint32_t a, std::uint32_t b) { + return (start[a + 1] - start[a]) > (start[b + 1] - start[b]); + }); + + // The last buckets face a nearly full table. A bucket of size `s` with a + // fraction `phi` of the slots free needs about `phi^-s` trials, so the + // budget has to be generous; it is a diagnostic, not a working limit. + const std::uint32_t max_tries = 1u << 20; + + std::vector occupied(std::size_t(slots), 0); + std::vector placed; + placed.reserve(64); + + for (auto b : order) { + auto first = start[b], last = start[b + 1]; + + if (first == last) { + pilots[b] = 0; + + continue; + } + + bool done = false; + + for (std::uint32_t k = 0; k != max_tries; ++k) { + auto pilot = pilot_at(k); + placed.clear(); + bool ok = true; + + for (auto m = first; m != last; ++m) { + auto index = place(keys[members[m]], pilot, slots); + + if (occupied[index] || + std::find(placed.begin(), placed.end(), index) != + placed.end()) { + ok = false; + + break; + } + + placed.push_back(index); + } + + if (ok) { + for (auto index : placed) { + occupied[index] = 1; + } + + pilots[b] = pilot; + done = true; + + break; + } + } + + if (!done) { + return false; + } + } + + return true; +} + +template +template +template +auto minimal_perfect_hash::fn:: + initialize(const Context& ctx, const std::tuple& options) + -> void { + (void)options; + + std::vector ids; + + for (auto iter = ctx.classes_begin(); iter != ctx.classes_end(); ++iter) { + for (auto type_iter = iter->type_id_begin(); + type_iter != iter->type_id_end(); ++type_iter) { + ids.push_back( + std::uint64_t(reinterpret_cast(*type_iter))); + } + } + + // One class may be registered under the same type id by several modules; + // the table is over *distinct* ids. + std::sort(ids.begin(), ids.end()); + ids.erase(std::unique(ids.begin(), ids.end()), ids.end()); + + // Zero is a fixed point of the multiply: it lands in slot 0 for every seed and every pilot, + // so it cannot be displaced and the search fails spuriously whenever + // another bucket has taken that slot. A type id of zero is therefore + // outside this policy's domain - see the class documentation. `ids` is + // sorted, so one comparison settles it. + if constexpr (Registry::has_runtime_checks) { + BOOST_ASSERT(ids.empty() || ids.front() != 0); + } + + auto n = ids.size(); + + if (n == 0) { + st().seed = 1; + st().bucket_shift = 63; + st().size = 0; + st().pilots.assign(2, 0); + + return; + } + + // Slots. `LoadPercent = 100` asks for exactly one per type id. + auto slots = (n * 100 + LoadPercent - 1) / LoadPercent; + + // Smallest power of two of at least ceil(n / Lambda) buckets, and never + // fewer than two, so the shift stays below 64. + std::size_t buckets = 2; + std::size_t log_buckets = 1; + + while (buckets * Lambda < n) { + buckets <<= 1; + ++log_buckets; + } + + std::vector pilots(buckets); + std::size_t bucket_shift = 64 - log_buckets; + std::size_t seeds = 0; + bool found = false; + + for (; seeds != MaxSeeds; ++seeds) { + auto seed = mix_seed(seeds); + + if (build(ids, seed, bucket_shift, slots, pilots)) { + st().seed = seed; + found = true; + ++seeds; + + break; + } + } + + if (!found) { + search_error error; + error.classes = n; + error.seeds = seeds; + + if constexpr (Registry::has_error_handler) { + Registry::error_handler::error(error); + } + + abort(); + } + + st().bucket_shift = bucket_shift; + st().size = slots; + st().pilots = std::move(pilots); + + if constexpr (Context::template has_option) { + ctx.tr << " type ids: " << n << ", buckets: " << buckets + << ", seeds tried: " << seeds << ", table: " << slots + << " slots + " << buckets << " pilots\n"; + } + + if constexpr (Registry::has_runtime_checks) { + st().control.assign(std::size_t(slots), type_id(detail::uintptr_max)); + + for (auto id : ids) { + auto h = id * st().seed; + auto pilot = st().pilots[std::size_t(h >> bucket_shift)]; + st().control[place(h, pilot, slots)] = + reinterpret_cast(id); + } + } +} + +template +template +void minimal_perfect_hash::fn::check( + std::size_t index, type_id type) { + if (index >= st().size || st().control[index] != type) { + if constexpr (Registry::has_error_handler) { + missing_class error; + error.type = type; + Registry::error_handler::error(error); + } + + abort(); + } +} + +template +template +auto minimal_perfect_hash::search_error::write( + Stream& os) const -> void { + os << "could not place " << classes << " type ids after trying " << seeds + << " multipliers\n"; +} + +} // namespace boost::openmethod::policies + +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +#endif diff --git a/include/boost/openmethod/policies/two_level_hash.hpp b/include/boost/openmethod/policies/two_level_hash.hpp new file mode 100644 index 00000000..43d653b5 --- /dev/null +++ b/include/boost/openmethod/policies/two_level_hash.hpp @@ -0,0 +1,461 @@ +// 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) + +#ifndef BOOST_OPENMETHOD_POLICY_TWO_LEVEL_HASH_HPP +#define BOOST_OPENMETHOD_POLICY_TWO_LEVEL_HASH_HPP + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifdef _MSC_VER +#pragma warning(push) +// 4702: unreachable code. The `abort()` after a call to the error handler is +// there for a handler that returns - the default one prints and returns - but a +// handler that is [[noreturn]], like throw_error_handler, makes it dead code, +// and MSVC diagnoses that. Same reason as in preamble.hpp and core.hpp. +#pragma warning(disable : 4702) +#endif + +namespace boost::openmethod::policies { + +//! Map type ids to an index with two multiply-shifts. +//! +//! `two_level_hash` implements the @ref type_hash policy with a per-bucket +//! multiplier into a shared power-of-two table: +//! +//! @code +//! h = m1 * x; // mix once +//! h1(x) = h >> s1; // which bucket, 2^b of them +//! h2(x) = (m2[h1(x)] * h) >> s2; // the index, into 2^t slots +//! @endcode +//! +//! The first level is one imperfect multiply-shift into a fixed number of +//! buckets; the second is a *per-bucket* multiplier, found by trial, that sends +//! every id in its bucket to a slot that is still free. +//! +//! It is @ref minimal_perfect_hash with one thing changed: where that reduces +//! with the top half of a product, onto a table of any size, this one shifts, +//! onto a table whose size is a power of two. The shift is the cheaper of the +//! two where the compiler hoists the shift amount out of the dispatch loop, +//! which is worth about a nanosecond per call; where it reloads it on every +//! call the two cost the same. Both are slower than @ref fast_perfect_hash. +//! +//! What the power-of-two table costs is a *sawtooth*: it holds +//! `2^ceil(log2(n))` slots, so between 1.0 and 2.0 per type id depending on +//! where `n` falls relative to a power of two, against a flat +//! `100 / LoadPercent` for @ref minimal_perfect_hash. Which of the two is +//! smaller is decided by the class count, which a program does not usually +//! control. Prefer @ref minimal_perfect_hash when the footprint has to be +//! predictable, and this one when the dispatch path matters more. +//! +//! The second multiply has to be applied to `h`, not to `x` itself. Two type +//! ids in one module are a few tens of bytes apart, so `m2 * x` differs between +//! them by about `m2 * 16`; for a 32-bit `m2` that is below `2^(64 - t)`, the +//! shift discards it, and the two land on the same slot for every multiplier +//! the search can try. Multiplying the already-mixed `h` costs nothing, since +//! `h` is ready long before `m2` arrives from the table. +//! +//! Like @ref minimal_perfect_hash this needs no instruction-set extension and +//! makes no assumption about the layout of the type ids. +//! +//! @note **A type id of zero is outside this policy's domain**, exactly as it is +//! for @ref minimal_perfect_hash. Zero is a fixed point of both multiplies, so +//! it lands in slot 0 whatever `m1` and the per-bucket multiplier are, and the +//! search fails whenever another bucket has taken that slot. Addresses are never zero; a custom @ref rtti policy handing out small +//! integers must not use zero. When the registry has @ref runtime_checks, +//! @ref initialize asserts that none of the registered type ids is zero. +//! +//! @tparam Lambda Average bucket size. +//! @tparam MaxDoublings How far the table may grow past the smallest power of +//! two that could hold the type ids, before @ref search_error is reported. +//! The cap is relative to the class count deliberately: an absolute one lets +//! a pathological input ask for a table orders of magnitude larger than the +//! program needs. +//! +//! @par Example +//! include:policies.cpp#two_level_hash +//! +//! @see [Registries and Policies](xref:ROOT:registries_and_policies.adoc) +template +struct two_level_hash : type_hash { + + //! No table within `MaxDoublings` doublings admitted a complete + //! assignment. + struct search_error : openmethod_error { + //! Number of registered type ids. + std::size_t classes; + //! Widest table tried. + std::size_t table_bits; + + template + auto write(Stream& os) const -> void; + }; + + using errors = std::variant; + + static_assert(Lambda > 0); + + //! `state` layout when runtime checks are disabled. + struct no_checks { + //! First-level multiplier. + std::uint64_t m1; + //! `64 - b`. + std::size_t s1; + //! `64 - t`. + std::size_t s2; + //! Second-level multiplier, one per bucket. + std::vector m2; + //! `2^t`. + std::size_t slots; + }; + + //! `state` layout when runtime checks are enabled: adds the table of + //! registered type ids used to validate hashed types. + struct with_checks : no_checks { + std::vector control; + }; + + //! A TypeHashFn metafunction. + //! + //! @tparam Registry The registry containing this policy + template + class fn { + public: + using state = std::conditional_t< + Registry::has_runtime_checks, with_checks, no_checks>; + + private: + static auto& st() { + return Registry::template state< + two_level_hash>(); + } + + static void check(std::size_t index, type_id type); + + // The second-level multiplier tried at step `k`. Odd, because an even + // multiplier throws away the key's top bits. + static auto m2_at(std::uint32_t k) -> std::uint32_t { + auto z = k * 0x9e3779b9u; + z ^= z >> 15; + z *= 0x85ebca6bu; + z ^= z >> 13; + + return z | 1u; + } + + static auto m1_at(std::uint64_t z) -> std::uint64_t { + z += 0x9e3779b97f4a7c15ull; + z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9ull; + z = (z ^ (z >> 27)) * 0x94d049bb133111ebull; + + return (z ^ (z >> 31)) | 1; + } + + static auto build( + const std::vector& ids, std::uint64_t m1, + std::size_t s1, std::size_t s2, std::size_t slots, + std::vector& m2) -> bool; + + public: + //! Finds `m1` and the per-bucket multipliers. + //! + //! @tparam Context An @ref InitializeContext. + //! @param ctx A Context object. + //! @param options A tuple of option objects. + template + static auto initialize( + const Context& ctx, const std::tuple& options) -> void; + + //! Returns the hash range: `[0, slots - 1]`. + static auto hash_range() -> std::pair { + return std::pair{std::size_t(0), st().slots ? st().slots - 1 : 0}; + } + + //! Map a type id to an index + //! + //! @param type The type_id to map + //! @return The index + BOOST_FORCEINLINE + static auto hash(type_id type) -> std::size_t { + auto h = std::uint64_t(reinterpret_cast(type)) * + st().m1; + auto bucket = std::size_t(h >> st().s1); + auto index = + std::size_t((std::uint64_t(st().m2[bucket]) * h) >> st().s2); + + if constexpr (Registry::has_runtime_checks) { + check(index, type); + } + + return index; + } + + //! Releases the multiplier table, and the control table if there is + //! one. + template + static auto finalize(const std::tuple& options) -> void { + (void)options; + + st().m2.clear(); + st().m2.shrink_to_fit(); + st().slots = 0; + + if constexpr (Registry::has_runtime_checks) { + st().control.clear(); + st().control.shrink_to_fit(); + } + } + }; +}; + +template +template +auto two_level_hash::fn::build( + const std::vector& ids, std::uint64_t m1, std::size_t s1, + std::size_t s2, std::size_t slots, std::vector& m2) -> bool { + auto n = ids.size(); + auto buckets = m2.size(); + + // Group by bucket: counting sort into a CSR-style pair of arrays. + std::vector keys(n); + std::vector bucket_of(n); + + for (std::size_t i = 0; i != n; ++i) { + keys[i] = m1 * ids[i]; + bucket_of[i] = std::uint32_t(keys[i] >> s1); + } + + std::vector start(buckets + 1, 0); + + for (auto b : bucket_of) { + ++start[b + 1]; + } + + std::partial_sum(start.begin(), start.end(), start.begin()); + std::vector members(n); + auto fill = start; + + for (std::size_t i = 0; i != n; ++i) { + members[fill[bucket_of[i]]++] = std::uint32_t(i); + } + + // Largest buckets first: a big bucket is placeable only while the table + // is still mostly empty. + std::vector order(buckets); + std::iota(order.begin(), order.end(), std::uint32_t(0)); + std::stable_sort( + order.begin(), order.end(), [&](std::uint32_t a, std::uint32_t b) { + return (start[a + 1] - start[a]) > (start[b + 1] - start[b]); + }); + + const std::uint32_t max_tries = 1u << 20; + std::vector occupied(slots, 0); + std::vector placed; + placed.reserve(64); + + for (auto b : order) { + auto first = start[b], last = start[b + 1]; + + if (first == last) { + m2[b] = 1; + + continue; + } + + bool done = false; + + for (std::uint32_t k = 0; k != max_tries; ++k) { + auto candidate = m2_at(k); + placed.clear(); + bool ok = true; + + for (auto m = first; m != last; ++m) { + auto index = std::size_t( + (std::uint64_t(candidate) * keys[members[m]]) >> s2); + + if (occupied[index] || + std::find(placed.begin(), placed.end(), index) != + placed.end()) { + ok = false; + + break; + } + + placed.push_back(index); + } + + if (ok) { + for (auto index : placed) { + occupied[index] = 1; + } + + m2[b] = candidate; + done = true; + + break; + } + } + + if (!done) { + return false; + } + } + + return true; +} + +template +template +template +auto two_level_hash::fn::initialize( + const Context& ctx, const std::tuple& options) -> void { + (void)options; + + std::vector ids; + + for (auto iter = ctx.classes_begin(); iter != ctx.classes_end(); ++iter) { + for (auto type_iter = iter->type_id_begin(); + type_iter != iter->type_id_end(); ++type_iter) { + ids.push_back( + std::uint64_t(reinterpret_cast(*type_iter))); + } + } + + // One class may be registered under the same type id by several modules; + // the table is over *distinct* ids. + std::sort(ids.begin(), ids.end()); + ids.erase(std::unique(ids.begin(), ids.end()), ids.end()); + + // Zero is a fixed point of the multiply: it lands in slot 0 whatever `m1` and the per-bucket multiplier are, + // so it cannot be displaced and the search fails spuriously whenever + // another bucket has taken that slot. A type id of zero is therefore + // outside this policy's domain - see the class documentation. `ids` is + // sorted, so one comparison settles it. + if constexpr (Registry::has_runtime_checks) { + BOOST_ASSERT(ids.empty() || ids.front() != 0); + } + + auto n = ids.size(); + + if (n == 0) { + st().m1 = 1; + st().s1 = 63; + st().s2 = 63; + st().m2.assign(2, 1); + st().slots = 0; + + return; + } + + // Never fewer than two buckets, so the shift stays below 64. + std::size_t buckets = 2, b = 1; + + while (buckets * Lambda < n) { + buckets <<= 1; + ++b; + } + + // The first-level multiplier is taken as it comes. Scoring several and + // keeping the one that spreads the buckets most evenly was measured, and + // does not pay: the placement cost is set by the tail, where every + // remaining bucket faces an almost full table, and evening the bucket + // sizes does not change how full that table is. + auto m1 = m1_at(0); + + // Smallest power-of-two table that can hold them, grown on failure. + std::size_t t = 1; + + while ((std::size_t(1) << t) < n) { + ++t; + } + + std::vector m2(buckets); + bool found = false; + + const std::size_t max_t = t + MaxDoublings; + + for (; t <= max_t; ++t) { + if (build(ids, m1, 64 - b, 64 - t, std::size_t(1) << t, m2)) { + found = true; + + break; + } + } + + if (!found) { + search_error error; + error.classes = n; + error.table_bits = max_t; + + if constexpr (Registry::has_error_handler) { + Registry::error_handler::error(error); + } + + abort(); + } + + st().m1 = m1; + st().s1 = 64 - b; + st().s2 = 64 - t; + st().slots = std::size_t(1) << t; + st().m2 = std::move(m2); + + if constexpr (Context::template has_option) { + ctx.tr << " type ids: " << n << ", buckets: " << buckets + << ", table: " << st().slots << " slots\n"; + } + + if constexpr (Registry::has_runtime_checks) { + st().control.assign(st().slots, type_id(detail::uintptr_max)); + + for (auto id : ids) { + auto h = st().m1 * id; + auto bucket = std::size_t(h >> st().s1); + auto index = + std::size_t((std::uint64_t(st().m2[bucket]) * h) >> st().s2); + st().control[index] = reinterpret_cast(id); + } + } +} + +template +template +void two_level_hash::fn::check( + std::size_t index, type_id type) { + if (index >= st().slots || st().control[index] != type) { + if constexpr (Registry::has_error_handler) { + missing_class error; + error.type = type; + Registry::error_handler::error(error); + } + + abort(); + } +} + +template +template +auto two_level_hash::search_error::write(Stream& os) const + -> void { + os << "could not place " << classes << " type ids in a table of up to 2^" + << table_bits << " slots\n"; +} + +} // namespace boost::openmethod::policies + +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +#endif diff --git a/include/boost/openmethod/policies/vptr_map.hpp b/include/boost/openmethod/policies/vptr_map.hpp index 4c4834d5..0cbbcd5e 100644 --- a/include/boost/openmethod/policies/vptr_map.hpp +++ b/include/boost/openmethod/policies/vptr_map.hpp @@ -11,6 +11,15 @@ #include #include +#ifdef _MSC_VER +#pragma warning(push) +// 4702: unreachable code. The `abort()` after a call to the error handler is +// there for a handler that returns - the default one prints and returns - but a +// handler that is [[noreturn]], like throw_error_handler, makes it dead code, +// and MSVC diagnoses that. Same reason as in preamble.hpp and core.hpp. +#pragma warning(disable : 4702) +#endif + namespace boost::openmethod { namespace policies { @@ -153,4 +162,8 @@ class vptr_map : public vptr { } // namespace policies } // namespace boost::openmethod +#ifdef _MSC_VER +#pragma warning(pop) +#endif + #endif diff --git a/include/boost/openmethod/policies/vptr_vector.hpp b/include/boost/openmethod/policies/vptr_vector.hpp index 493f695a..daabfebc 100644 --- a/include/boost/openmethod/policies/vptr_vector.hpp +++ b/include/boost/openmethod/policies/vptr_vector.hpp @@ -12,6 +12,15 @@ #include #include +#ifdef _MSC_VER +#pragma warning(push) +// 4702: unreachable code. The `abort()` after a call to the error handler is +// there for a handler that returns - the default one prints and returns - but a +// handler that is [[noreturn]], like throw_error_handler, makes it dead code, +// and MSVC diagnoses that. Same reason as in preamble.hpp and core.hpp. +#pragma warning(disable : 4702) +#endif + namespace boost::openmethod { namespace policies { @@ -232,4 +241,8 @@ struct vptr_vector : vptr { } // namespace policies } // namespace boost::openmethod +#ifdef _MSC_VER +#pragma warning(pop) +#endif + #endif diff --git a/include/boost/openmethod/preamble.hpp b/include/boost/openmethod/preamble.hpp index 5624d42e..c1f6f24f 100644 --- a/include/boost/openmethod/preamble.hpp +++ b/include/boost/openmethod/preamble.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -26,6 +27,26 @@ namespace boost::openmethod { +// ----------------------------------------------------------------------------- +// uintptr + +namespace detail { + +// The unsigned integer a type_id can be reinterpreted as. Every `type_hash` +// policy needs it, so it lives here rather than in any one of them. +#if defined(UINTPTR_MAX) +using uintptr = std::uintptr_t; +constexpr uintptr uintptr_max = UINTPTR_MAX; +#else +static_assert( + sizeof(std::size_t) == sizeof(void*), + "This implementation requires that size_t and void* have the same size."); +using uintptr = std::size_t; +constexpr uintptr uintptr_max = (std::numeric_limits::max)(); +#endif + +} // namespace detail + // ----------------------------------------------------------------------------- // word diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 85954411..e493cb5e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -105,6 +105,25 @@ foreach(test_cpp ${test_cpp_files}) target_compile_options(${test_target} PRIVATE -Wa,-mbig-obj) endif() + # minimal_cover_hash dispatches with BMI2's pext, and because `hash` is + # inlined into every call the instruction has to be enabled for the whole + # translation unit. Only the tests that name the policy need it, and only on + # x86-64 - the 64-bit pext intrinsic does not exist in 32-bit mode: + # elsewhere BOOST_OPENMETHOD_HAS_PEXT is 0 and the policy drops out of both. + set(test_needs_bmi2 FALSE) + + if (test MATCHES "minimal_cover_hash|hash_policies" AND + CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$") + set(test_needs_bmi2 TRUE) + + if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" OR + CMAKE_CXX_COMPILER_FRONTEND_VARIANT MATCHES "MSVC") + target_compile_options(${test_target} PRIVATE /arch:AVX2) + else() + target_compile_options(${test_target} PRIVATE -mbmi2) + endif() + endif() + file(READ ${test_cpp} test_cpp_contents) set(test_cpp_overrides_registry -1) foreach(marker "BOOST_OPENMETHOD_DEFAULT_REGISTRY" "test_capture_errors.hpp" @@ -115,7 +134,11 @@ foreach(test_cpp ${test_cpp_files}) endif() endforeach() - if (test_cpp_overrides_registry EQUAL -1) + # The BMI2 sources are left out too, for a different reason: the shared PCH + # is compiled without the flag, and GCC then warns (-Winvalid-pch) that it + # was "created and used with differing settings of '-mbmi2'" for every file + # that reuses it with the flag on. + if (test_cpp_overrides_registry EQUAL -1 AND NOT test_needs_bmi2) if (NOT boost_openmethod_pch_owner) target_precompile_headers(${test_target} PRIVATE ${BOOST_OPENMETHOD_TEST_PCH_HEADERS}) set(boost_openmethod_pch_owner ${test_target}) @@ -192,7 +215,14 @@ foreach(compile_fail_cpp ${compile_fail_cpp_files}) message(FATAL_ERROR "${testname}.cpp has no `// expected-error: ` comment") endif() - openmethod_compile_fail_test(${testname} "${CMAKE_MATCH_1}") + set(fail_regex "${CMAKE_MATCH_1}") + # PASS_REGULAR_EXPRESSION is a list: a `;` would split the regex into + # alternatives, and the test would pass on either half. + if (fail_regex MATCHES ";") + message(FATAL_ERROR + "${testname}.cpp: the expected-error regex contains a `;`; use `.*`") + endif() + openmethod_compile_fail_test(${testname} "${fail_regex}") endforeach() if (TARGET Boost::dll) diff --git a/test/Jamfile b/test/Jamfile index f55d73d1..1281c244 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -78,11 +78,32 @@ alias unit_test_framework /boost/test//boost_unit_test_framework/off ; -for local src in [ glob test_*.cpp ] +local bmi2-sources = + test_dispatch_minimal_cover_hash.cpp + test_hash_policies.cpp + ; + +for local src in [ glob test_*.cpp : $(bmi2-sources) ] { run $(src) unit_test_framework ; } +# minimal_cover_hash dispatches with BMI2's pext, and because `hash` is inlined +# into every call the instruction has to be enabled for the whole translation +# unit - so the sources that name the policy are declared on their own, rather +# than through the glob above. Where the probe fails - ARM, or a compiler that +# will not take the flag - BOOST_OPENMETHOD_HAS_PEXT is 0 and the policy drops +# out of both files, so nothing here is conditional on the outcome except the +# flag itself. +for local src in $(bmi2-sources) +{ + run $(src) unit_test_framework + : : : + [ check-target-builds /boost/openmethod/config//has_bmi2 + "BMI2 pext" : -mbmi2 ] + ; +} + run mix_release_debug/main.cpp mix_release_debug/lib.cpp unit_test_framework ; diff --git a/test/compile_fail_final_virtual_ptr_weak_ptr.cpp b/test/compile_fail_final_virtual_ptr_weak_ptr.cpp new file mode 100644 index 00000000..de996e59 --- /dev/null +++ b/test/compile_fail_final_virtual_ptr_weak_ptr.cpp @@ -0,0 +1,26 @@ +// 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) + +// Expected diagnostic, as a CMake regex (see CMakeLists.txt). +// expected-error: cannot be wrapped in a virtual_ptr + +#include +#include + +using namespace boost::openmethod; + +struct Animal { + virtual ~Animal() { + } +}; + +BOOST_OPENMETHOD_CLASSES(Animal); + +int main() { + auto felix = std::make_shared(); + std::weak_ptr weak = felix; + auto p = final_virtual_ptr(weak); + return 0; +} diff --git a/test/compile_fail_member_overrider_parameter_mismatch.cpp b/test/compile_fail_member_overrider_parameter_mismatch.cpp new file mode 100644 index 00000000..9a90bdf1 --- /dev/null +++ b/test/compile_fail_member_overrider_parameter_mismatch.cpp @@ -0,0 +1,41 @@ +// 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) + +// Expected diagnostic, as a CMake regex (see CMakeLists.txt). +// expected-error: must be an unambiguous accessible base + +#include + +using namespace boost::openmethod; + +class Animal { + public: + virtual ~Animal() = default; +}; + +class Cat : private Animal { + public: + Animal& as_animal() { + return *this; + } +}; + +BOOST_OPENMETHOD(poke, (virtual_ptr), void); + +// A member overrider goes through the same override_aux/override_impl and +// validate_overrider_parameter machinery as a free-function one +// (BOOST_OPENMETHOD_OVERRIDE_FN is sugar over BOOST_OPENMETHOD_REGISTER and +// BOOST_OPENMETHOD_TYPE, not a new validation path), so a mismatched +// parameter is caught the same way. +class Handler { + static auto poke_cat(virtual_ptr) -> void { + } + + BOOST_OPENMETHOD_OVERRIDE_FN( + poke, (virtual_ptr), void, &Handler::poke_cat); +}; + +int main() { +} diff --git a/test/compile_fail_weak_ptr_parameter.cpp b/test/compile_fail_weak_ptr_parameter.cpp new file mode 100644 index 00000000..69c79f9a --- /dev/null +++ b/test/compile_fail_weak_ptr_parameter.cpp @@ -0,0 +1,30 @@ +// 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) + +// Expected diagnostic, as a CMake regex (see CMakeLists.txt). +// A `;` would split the regex into two alternatives (see CMakeLists.txt). +// expected-error: a weak pointer cannot be a virtual parameter.*call lock\(\) first + +#include +#include + +using namespace boost::openmethod; + +struct Animal { + virtual ~Animal() { + } +}; +struct Cat : Animal {}; + +BOOST_OPENMETHOD(poke, (virtual_>), void); + +BOOST_OPENMETHOD_OVERRIDE(poke, (std::weak_ptr), void) { +} + +int main() { + auto felix = std::make_shared(); + poke(std::weak_ptr(felix)); + return 0; +} diff --git a/test/compile_fail_weak_virtual_ptr_parameter.cpp b/test/compile_fail_weak_virtual_ptr_parameter.cpp new file mode 100644 index 00000000..99cb2d3b --- /dev/null +++ b/test/compile_fail_weak_virtual_ptr_parameter.cpp @@ -0,0 +1,30 @@ +// 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) + +// Expected diagnostic, as a CMake regex (see CMakeLists.txt). +// A `;` would split the regex into two alternatives (see CMakeLists.txt). +// expected-error: a weak pointer cannot be a virtual parameter.*call lock\(\) first + +#include +#include + +using namespace boost::openmethod; + +struct Animal { + virtual ~Animal() { + } +}; +struct Cat : Animal {}; + +BOOST_OPENMETHOD(poke, (virtual_>), void); + +BOOST_OPENMETHOD_OVERRIDE(poke, (weak_virtual_ptr), void) { +} + +int main() { + auto felix = std::make_shared(); + poke(weak_virtual_ptr(felix)); + return 0; +} diff --git a/test/test_adl_registry_smart_ptr.cpp b/test/test_adl_registry_smart_ptr.cpp index 2137433f..b51d4454 100644 --- a/test/test_adl_registry_smart_ptr.cpp +++ b/test/test_adl_registry_smart_ptr.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -59,6 +60,11 @@ static_assert(std::is_same_v< boost_intrusive_virtual_ptr, virtual_ptr, zoo_registry>>); +// weak_virtual_ptr is not a virtual_ptr, but it defaults its registry the +// same way, so it converts to and from the shared_virtual_ptr of its class +static_assert( + std::is_same_v, weak_virtual_ptr>); + BOOST_OPENMETHOD_CLASSES(Animal, Dog, zoo_registry); BOOST_OPENMETHOD(name, (shared_virtual_ptr), std::string); @@ -76,4 +82,9 @@ BOOST_AUTO_TEST_CASE(factories_need_no_registry_argument) { auto owned = make_unique_virtual(); static_assert(std::is_same_v>); + + weak_virtual_ptr observer = dog; + static_assert( + std::is_same_v>); + BOOST_TEST(name(observer.lock()) == "dog"); } diff --git a/test/test_dispatch_boost_any.cpp b/test/test_dispatch_boost_any.cpp index d2cbfcda..6f0d40de 100644 --- a/test/test_dispatch_boost_any.cpp +++ b/test/test_dispatch_boost_any.cpp @@ -32,6 +32,18 @@ static_assert(detail::has_vptr< virtual_traits, const boost::any&>); +// A registry spelled on the parameter names the same parameter as the default +// one, in each of the three forms. +static_assert(detail::validate_method_parameter< + virtual_, default_registry, + void>::value); +static_assert(detail::validate_method_parameter< + virtual_, default_registry, + void>::value); +static_assert(detail::validate_method_parameter< + virtual_, default_registry, + void>::value); + MAKE_CLASSES(); BOOST_OPENMETHOD(name, (virtual_), std::string); diff --git a/test/test_dispatch_minimal_cover_hash.cpp b/test/test_dispatch_minimal_cover_hash.cpp new file mode 100644 index 00000000..5fd79dd4 --- /dev/null +++ b/test/test_dispatch_minimal_cover_hash.cpp @@ -0,0 +1,155 @@ +// 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) + +// First, for BOOST_OPENMETHOD_HAS_PEXT. This header pulls in preamble.hpp but +// not core.hpp, so the default-registry override below is still in time. +#include + +// `minimal_cover_hash` dispatches with BMI2's `pext`, which not every target +// has; naming the policy in a registry where it is absent is a compile error, +// by design. So the recipe and the cases are conditional, and on a target +// without the instruction this file still builds - as a single case that +// records why it did nothing. The build files add `-mbmi2` (or `/arch:AVX2`) to +// this translation unit alone, on x86 only. +#if BOOST_OPENMETHOD_HAS_PEXT + +struct test_registry; +#define BOOST_OPENMETHOD_DEFAULT_REGISTRY test_registry + +#include +#include +#include + +// `runtime_checks` unconditionally, rather than only in a Debug build, so that +// the control table `hash` consults is exercised whatever the build type; and +// `throw_error_handler` so that a lookup of an unregistered class is observable +// from a test case instead of aborting. +struct test_registry : + boost::openmethod::default_registry::with< + boost::openmethod::policies::minimal_cover_hash<>, + boost::openmethod::policies::runtime_checks, + boost::openmethod::policies::throw_error_handler> {}; + +#endif + +#define BOOST_TEST_MODULE dispatch_minimal_cover_hash +#include + +#if BOOST_OPENMETHOD_HAS_PEXT + +#include +#include + +using namespace boost::openmethod; + +namespace { + +struct Animal { + virtual ~Animal() = default; +}; + +struct Dog : Animal {}; +struct Cat : Animal {}; +struct Bulldog : Dog {}; +struct Tiger : Cat {}; + +// Registered nowhere below: calling with one of these must be diagnosed. +struct Ghost : Animal {}; + +} // namespace + +// Withholding `Ghost` is the point of one of the cases, so register explicitly +// and do not call BOOST_OPENMETHOD_REGISTER_CLASSES - see test_classes.hpp. +BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat, Bulldog, Tiger); + +BOOST_OPENMETHOD(name, (virtual_), std::string); +BOOST_OPENMETHOD_OVERRIDE(name, (const Animal&), std::string) { + return "animal"; +} +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog&), std::string) { + return "dog"; +} +BOOST_OPENMETHOD_OVERRIDE(name, (const Cat&), std::string) { + return "cat"; +} +BOOST_OPENMETHOD_OVERRIDE(name, (const Bulldog&), std::string) { + return "bulldog"; +} + +BOOST_OPENMETHOD( + meet, (virtual_, virtual_), std::string); +BOOST_OPENMETHOD_OVERRIDE(meet, (const Animal&, const Animal&), std::string) { + return "ignore"; +} +BOOST_OPENMETHOD_OVERRIDE(meet, (const Dog&, const Cat&), std::string) { + return "chase"; +} +BOOST_OPENMETHOD_OVERRIDE(meet, (const Cat&, const Dog&), std::string) { + return "hiss"; +} + +using type_hash = test_registry::policy; + +BOOST_AUTO_TEST_CASE(single_dispatch) { + initialize(); + + BOOST_TEST(name(Animal()) == "animal"); + BOOST_TEST(name(Dog()) == "dog"); + BOOST_TEST(name(Cat()) == "cat"); + BOOST_TEST(name(Bulldog()) == "bulldog"); + BOOST_TEST(name(Tiger()) == "cat"); +} + +BOOST_AUTO_TEST_CASE(multiple_dispatch) { + initialize(); + + BOOST_TEST(meet(Dog(), Cat()) == "chase"); + BOOST_TEST(meet(Cat(), Dog()) == "hiss"); + BOOST_TEST(meet(Bulldog(), Tiger()) == "chase"); + BOOST_TEST(meet(Dog(), Dog()) == "ignore"); +} + +// The property the policy exists for: a cover of bit positions that still +// separates the registered type ids. +BOOST_AUTO_TEST_CASE(hash_is_injective) { + initialize(); + + auto [low, high] = type_hash::hash_range(); + BOOST_TEST(low == 0u); + + std::set seen; + + for (auto type : + {&typeid(Animal), &typeid(Dog), &typeid(Cat), &typeid(Bulldog), + &typeid(Tiger)}) { + auto index = type_hash::hash(type); + BOOST_TEST(index >= low); + BOOST_TEST(index <= high); + BOOST_TEST(seen.insert(index).second); + } + + // No minimality bound here: the table is `2^popcount(mask)` and the cover + // search minimizes the number of *bits*, not the number of slots, so the + // table can be much larger than the number of type ids. That is the + // trade this policy makes - see its documentation. + BOOST_TEST(high + 1 >= seen.size()); +} + +BOOST_AUTO_TEST_CASE(unregistered_class_is_diagnosed) { + initialize(); + + BOOST_CHECK_THROW(name(Ghost()), missing_class); +} + +#else + +BOOST_AUTO_TEST_CASE(pext_unavailable) { + BOOST_TEST_MESSAGE( + "minimal_cover_hash needs BMI2; BOOST_OPENMETHOD_HAS_PEXT is 0 on this " + "target, so there is nothing to test here"); + BOOST_TEST(BOOST_OPENMETHOD_HAS_PEXT == 0); +} + +#endif diff --git a/test/test_dispatch_minimal_perfect_hash.cpp b/test/test_dispatch_minimal_perfect_hash.cpp new file mode 100644 index 00000000..25cdb92c --- /dev/null +++ b/test/test_dispatch_minimal_perfect_hash.cpp @@ -0,0 +1,129 @@ +// 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) + +struct test_registry; +#define BOOST_OPENMETHOD_DEFAULT_REGISTRY test_registry + +#include +#include +#include +#include + +// `runtime_checks` unconditionally, rather than only in a Debug build, so that +// the control table `hash` consults is exercised whatever the build type; and +// `throw_error_handler` so that a lookup of an unregistered class is observable +// from a test case instead of aborting. +struct test_registry : + boost::openmethod::default_registry::with< + boost::openmethod::policies::minimal_perfect_hash<>, + boost::openmethod::policies::runtime_checks, + boost::openmethod::policies::throw_error_handler> {}; + +#define BOOST_TEST_MODULE dispatch_minimal_perfect_hash +#include + +#include +#include + +using namespace boost::openmethod; + +namespace { + +struct Animal { + virtual ~Animal() = default; +}; + +struct Dog : Animal {}; +struct Cat : Animal {}; +struct Bulldog : Dog {}; +struct Tiger : Cat {}; + +// Registered nowhere below: calling with one of these must be diagnosed. +struct Ghost : Animal {}; + +} // namespace + +// Withholding `Ghost` is the point of one of the cases, so register explicitly +// and do not call BOOST_OPENMETHOD_REGISTER_CLASSES - see test_classes.hpp. +BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat, Bulldog, Tiger); + +BOOST_OPENMETHOD(name, (virtual_), std::string); +BOOST_OPENMETHOD_OVERRIDE(name, (const Animal&), std::string) { + return "animal"; +} +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog&), std::string) { + return "dog"; +} +BOOST_OPENMETHOD_OVERRIDE(name, (const Cat&), std::string) { + return "cat"; +} +BOOST_OPENMETHOD_OVERRIDE(name, (const Bulldog&), std::string) { + return "bulldog"; +} + +BOOST_OPENMETHOD( + meet, (virtual_, virtual_), std::string); +BOOST_OPENMETHOD_OVERRIDE(meet, (const Animal&, const Animal&), std::string) { + return "ignore"; +} +BOOST_OPENMETHOD_OVERRIDE(meet, (const Dog&, const Cat&), std::string) { + return "chase"; +} +BOOST_OPENMETHOD_OVERRIDE(meet, (const Cat&, const Dog&), std::string) { + return "hiss"; +} + +using type_hash = test_registry::policy; + +BOOST_AUTO_TEST_CASE(single_dispatch) { + initialize(); + + BOOST_TEST(name(Animal()) == "animal"); + BOOST_TEST(name(Dog()) == "dog"); + BOOST_TEST(name(Cat()) == "cat"); + BOOST_TEST(name(Bulldog()) == "bulldog"); + BOOST_TEST(name(Tiger()) == "cat"); +} + +BOOST_AUTO_TEST_CASE(multiple_dispatch) { + initialize(); + + BOOST_TEST(meet(Dog(), Cat()) == "chase"); + BOOST_TEST(meet(Cat(), Dog()) == "hiss"); + BOOST_TEST(meet(Bulldog(), Tiger()) == "chase"); + BOOST_TEST(meet(Dog(), Dog()) == "ignore"); +} + +// The property the policy exists for: one slot per type id, and every +// registered id inside the advertised range, distinct from the others. +BOOST_AUTO_TEST_CASE(hash_is_injective_and_minimal) { + initialize(); + + auto [low, high] = type_hash::hash_range(); + BOOST_TEST(low == 0u); + + std::set seen; + + for (auto type : + {&typeid(Animal), &typeid(Dog), &typeid(Cat), &typeid(Bulldog), + &typeid(Tiger)}) { + auto index = type_hash::hash(type); + BOOST_TEST(index >= low); + BOOST_TEST(index <= high); + BOOST_TEST(seen.insert(index).second); + } + + // `LoadPercent` defaults to 95, so the table holds at most one slot in + // twenty more than there are type ids. `void` is registered too, hence the + // floor rather than an exact figure. + BOOST_TEST(high + 1 >= seen.size()); + BOOST_TEST(high + 1 <= seen.size() * 2); +} + +BOOST_AUTO_TEST_CASE(unregistered_class_is_diagnosed) { + initialize(); + + BOOST_CHECK_THROW(name(Ghost()), missing_class); +} diff --git a/test/test_dispatch_std_any.cpp b/test/test_dispatch_std_any.cpp index 5500b483..b35c805e 100644 --- a/test/test_dispatch_std_any.cpp +++ b/test/test_dispatch_std_any.cpp @@ -32,6 +32,18 @@ static_assert( detail::has_vptr< virtual_traits, const std::any&>); +// A registry spelled on the parameter names the same parameter as the default +// one, in each of the three forms. +static_assert(detail::validate_method_parameter< + virtual_, default_registry, + void>::value); +static_assert( + detail::validate_method_parameter< + virtual_, default_registry, void>::value); +static_assert( + detail::validate_method_parameter< + virtual_, default_registry, void>::value); + MAKE_CLASSES(); BOOST_OPENMETHOD(name, (virtual_), std::string); diff --git a/test/test_dispatch_two_level_hash.cpp b/test/test_dispatch_two_level_hash.cpp new file mode 100644 index 00000000..5a3343e5 --- /dev/null +++ b/test/test_dispatch_two_level_hash.cpp @@ -0,0 +1,129 @@ +// 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) + +struct test_registry; +#define BOOST_OPENMETHOD_DEFAULT_REGISTRY test_registry + +#include +#include +#include +#include + +// `runtime_checks` unconditionally, rather than only in a Debug build, so that +// the control table `hash` consults is exercised whatever the build type; and +// `throw_error_handler` so that a lookup of an unregistered class is observable +// from a test case instead of aborting. +struct test_registry : + boost::openmethod::default_registry::with< + boost::openmethod::policies::two_level_hash<>, + boost::openmethod::policies::runtime_checks, + boost::openmethod::policies::throw_error_handler> {}; + +#define BOOST_TEST_MODULE dispatch_two_level_hash +#include + +#include +#include + +using namespace boost::openmethod; + +namespace { + +struct Animal { + virtual ~Animal() = default; +}; + +struct Dog : Animal {}; +struct Cat : Animal {}; +struct Bulldog : Dog {}; +struct Tiger : Cat {}; + +// Registered nowhere below: calling with one of these must be diagnosed. +struct Ghost : Animal {}; + +} // namespace + +// Withholding `Ghost` is the point of one of the cases, so register explicitly +// and do not call BOOST_OPENMETHOD_REGISTER_CLASSES - see test_classes.hpp. +BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat, Bulldog, Tiger); + +BOOST_OPENMETHOD(name, (virtual_), std::string); +BOOST_OPENMETHOD_OVERRIDE(name, (const Animal&), std::string) { + return "animal"; +} +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog&), std::string) { + return "dog"; +} +BOOST_OPENMETHOD_OVERRIDE(name, (const Cat&), std::string) { + return "cat"; +} +BOOST_OPENMETHOD_OVERRIDE(name, (const Bulldog&), std::string) { + return "bulldog"; +} + +BOOST_OPENMETHOD( + meet, (virtual_, virtual_), std::string); +BOOST_OPENMETHOD_OVERRIDE(meet, (const Animal&, const Animal&), std::string) { + return "ignore"; +} +BOOST_OPENMETHOD_OVERRIDE(meet, (const Dog&, const Cat&), std::string) { + return "chase"; +} +BOOST_OPENMETHOD_OVERRIDE(meet, (const Cat&, const Dog&), std::string) { + return "hiss"; +} + +using type_hash = test_registry::policy; + +BOOST_AUTO_TEST_CASE(single_dispatch) { + initialize(); + + BOOST_TEST(name(Animal()) == "animal"); + BOOST_TEST(name(Dog()) == "dog"); + BOOST_TEST(name(Cat()) == "cat"); + BOOST_TEST(name(Bulldog()) == "bulldog"); + BOOST_TEST(name(Tiger()) == "cat"); +} + +BOOST_AUTO_TEST_CASE(multiple_dispatch) { + initialize(); + + BOOST_TEST(meet(Dog(), Cat()) == "chase"); + BOOST_TEST(meet(Cat(), Dog()) == "hiss"); + BOOST_TEST(meet(Bulldog(), Tiger()) == "chase"); + BOOST_TEST(meet(Dog(), Dog()) == "ignore"); +} + +// Every registered id inside the advertised range and distinct from the others, +// in a table that is a power of two - so between one and two slots per type id. +BOOST_AUTO_TEST_CASE(hash_is_injective_and_minimal) { + initialize(); + + auto [low, high] = type_hash::hash_range(); + BOOST_TEST(low == 0u); + + std::set seen; + + for (auto type : + {&typeid(Animal), &typeid(Dog), &typeid(Cat), &typeid(Bulldog), + &typeid(Tiger)}) { + auto index = type_hash::hash(type); + BOOST_TEST(index >= low); + BOOST_TEST(index <= high); + BOOST_TEST(seen.insert(index).second); + } + + // The table holds `2^ceil(log2(n))` slots, so between one and two per type + // id. `void` is registered too, hence the floor rather than an exact + // figure. + BOOST_TEST(high + 1 >= seen.size()); + BOOST_TEST(high + 1 <= seen.size() * 2); +} + +BOOST_AUTO_TEST_CASE(unregistered_class_is_diagnosed) { + initialize(); + + BOOST_CHECK_THROW(name(Ghost()), missing_class); +} diff --git a/test/test_dispatch_type_erasure.cpp b/test/test_dispatch_type_erasure.cpp index 83d82755..9554aa24 100644 --- a/test/test_dispatch_type_erasure.cpp +++ b/test/test_dispatch_type_erasure.cpp @@ -30,6 +30,24 @@ using erased_cref = te::any; static_assert(detail::has_vptr< virtual_traits, const erased&>); +// A registry spelled on the parameter names the same parameter as the default +// one, for the owning any by reference and for the any references. +static_assert(detail::validate_method_parameter< + virtual_, default_registry, + void>::value); +static_assert( + detail::validate_method_parameter< + virtual_, default_registry, void>::value); +static_assert( + detail::validate_method_parameter< + virtual_, default_registry, void>::value); +static_assert( + detail::validate_method_parameter< + virtual_, default_registry, void>::value); +static_assert(detail::validate_method_parameter< + virtual_, default_registry, + void>::value); + #define MAKE_CLASSES() \ struct Dog { \ std::string name; \ diff --git a/test/test_hash_policies.cpp b/test/test_hash_policies.cpp new file mode 100644 index 00000000..5e8a8a03 --- /dev/null +++ b/test/test_hash_policies.cpp @@ -0,0 +1,354 @@ +// 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) + +// Drives `type_hash` policies directly, through a stand-in for the +// InitializeContext blueprint, rather than through `initialize()`. That isolates +// a policy from the rest of the compiler and - the point of the exercise - lets +// the type ids be *chosen*, so that a distribution which is hard to hash can be +// presented deliberately instead of being whatever this program's own classes +// happen to get. +// +// The type ids here are fabricated addresses. No `type_hash` policy +// dereferences a type id - each only casts it to an integer - but the trace +// option would, so this file must never pass one, and the registries it +// declares must never be handed to `boost::openmethod::initialize()`. + +#include +#include +#include +#include + +#define BOOST_TEST_MODULE hash_policies +#include + +#include +#include +#include +#include +#include + +namespace bom = boost::openmethod; +namespace pol = boost::openmethod::policies; + +namespace { + +// A stand-in for InitializeContext. The policies under test use only +// classes_begin/classes_end, the type id range of each class, and has_option. +struct fake_class_view { + const bom::type_id* first; + const bom::type_id* last; + + auto type_id_begin() const { + return first; + } + + auto type_id_end() const { + return last; + } + + auto vptr() const -> bom::vptr_type { + return nullptr; + } + + auto static_vptr() const -> const bom::vptr_type* { + return nullptr; + } +}; + +struct fake_context { + template + static constexpr bool has_option = false; + + std::vector views; + + auto classes_begin() const { + return views.begin(); + } + + auto classes_end() const { + return views.end(); + } +}; + +// One class per type id, which is what augment_classes() produces for a program +// whose classes are each registered once. +auto context_over(const std::vector& ids) -> fake_context { + fake_context ctx; + ctx.views.reserve(ids.size()); + + for (const auto& id : ids) { + ctx.views.push_back(fake_class_view{&id, &id + 1}); + } + + return ctx; +} + +auto as_type_id(std::uintptr_t value) -> bom::type_id { + return reinterpret_cast(value); +} + +// The four distributions that matter, all on the 16-byte grid the Itanium ABI +// guarantees for `type_info` records. +// +// `packed` is one module whose records happen to be adjacent. `diluted` is the +// realistic single-module case: v-tables are emitted between the records, so +// they are spread over many times their own size. `multi_module` is a program +// plus implicitly linked libraries. `dlopened` is the case this family of +// policies exists for - a program plus modules the loader placed wherever it +// liked, at opposite ends of the address space. +// +// The bases are derived from the pointer width rather than written as literals. +// A type id is a pointer, and on a 32-bit target it is four bytes wide, so a +// 64-bit literal would be silently truncated - and bases that differ only in +// their high bits would collapse onto one another, leaving the generators +// producing duplicates. +constexpr auto address_bits = sizeof(std::uintptr_t) * 8; + +// `module` picks a distinct high-bit pattern; `spread` says how far apart the +// modules sit - a smaller value puts them further apart. `spread` must leave +// room for the largest pattern, so it is never less than 3 for four modules. +auto base_of(std::size_t module, std::size_t spread) -> std::uintptr_t { + return (std::uintptr_t(1 + module) << (address_bits - spread)) + 0x1000; +} + +// Each module gets a cursor that only ever moves forward, so the ids are +// distinct by construction. They have to be: a policy deduplicates the ids it +// is given, so a generator that repeated one would be testing the dedup rather +// than the hash, and would make an injectivity count come out short. +auto ids_over(std::size_t n, std::size_t modules, std::size_t spread) + -> std::vector { + std::vector at; + + for (std::size_t module = 0; module != modules; ++module) { + at.push_back(base_of(module, spread)); + } + + std::vector ids; + ids.reserve(n); + + for (std::size_t i = 0; i != n; ++i) { + auto module = i % modules; + ids.push_back(as_type_id(at[module])); + at[module] += 16 * (1 + (i * 2654435761u) % 24); + } + + return ids; +} + +auto ids_packed(std::size_t n) -> std::vector { + auto at = base_of(0, 8); + std::vector ids; + ids.reserve(n); + + for (std::size_t i = 0; i != n; ++i) { + ids.push_back(as_type_id(at)); + at += 16; + } + + return ids; +} + +auto ids_diluted(std::size_t n) -> std::vector { + return ids_over(n, 1, 8); +} + +auto ids_multi_module(std::size_t n) -> std::vector { + return ids_over(n, 4, 8); +} + +auto ids_dlopened(std::size_t n) -> std::vector { + return ids_over(n, 4, 3); +} + +// What every one of these policies promises: `hash` is injective over the type +// ids it was initialized with, and `hash_range` brackets every value it returns. +template +auto check_injective_over(const std::vector& ids) -> std::size_t { + using fn = typename Policy::template fn; + + auto ctx = context_over(ids); + fn::initialize(ctx, std::tuple<>{}); + + auto [low, high] = fn::hash_range(); + std::set seen; + + for (auto id : ids) { + auto index = fn::hash(id); + BOOST_TEST(index >= low); + BOOST_TEST(index <= high); + BOOST_TEST(seen.insert(index).second); + } + + BOOST_TEST(seen.size() == ids.size()); + fn::finalize(std::tuple<>{}); + + return high - low + 1; +} + +struct mph_registry : + bom::registry< + pol::std_rtti, pol::minimal_perfect_hash<>, pol::vptr_vector, + pol::default_error_handler, pol::stderr_output> {}; + +struct mph_minimal_registry : + bom::registry< + pol::std_rtti, pol::minimal_perfect_hash<2, 100>, pol::vptr_vector, + pol::default_error_handler, pol::stderr_output> {}; + +struct tlh_registry : + bom::registry< + pol::std_rtti, pol::two_level_hash<>, pol::vptr_vector, + pol::default_error_handler, pol::stderr_output> {}; + +#if BOOST_OPENMETHOD_HAS_PEXT +struct mch_registry : + bom::registry< + pol::std_rtti, pol::minimal_cover_hash<>, pol::vptr_vector, + pol::default_error_handler, pol::stderr_output> {}; +#endif + +} // namespace + +// The fixture's own precondition: a generator that repeated an id would make +// every injectivity count below come out short, for no fault of the policies. +BOOST_AUTO_TEST_CASE(generators_produce_distinct_ids) { + for (auto n : {std::size_t(1), std::size_t(17), std::size_t(1000)}) { + for (auto&& named : + {std::pair{"packed", ids_packed(n)}, + std::pair{"diluted", ids_diluted(n)}, + std::pair{"multi_module", ids_multi_module(n)}, + std::pair{"dlopened", ids_dlopened(n)}}) { + BOOST_TEST_CONTEXT(named.first << ", n = " << n) { + std::set distinct( + named.second.begin(), named.second.end()); + BOOST_TEST(distinct.size() == n); + } + } + } +} + +BOOST_AUTO_TEST_CASE(injective_on_every_distribution) { + for (auto n : + {std::size_t(1), std::size_t(2), std::size_t(17), std::size_t(256), + std::size_t(1000)}) { + for (auto&& named : + {std::pair{"packed", ids_packed(n)}, + std::pair{"diluted", ids_diluted(n)}, + std::pair{"multi_module", ids_multi_module(n)}, + std::pair{"dlopened", ids_dlopened(n)}}) { + BOOST_TEST_CONTEXT(named.first << ", n = " << n) { + check_injective_over>( + named.second); + check_injective_over>( + named.second); +#if BOOST_OPENMETHOD_HAS_PEXT + check_injective_over>( + named.second); +#endif + } + } + } +} + +// The property that distinguishes this family: the table is sized by how many +// type ids there are, not by where they sit. The `dlopened` distribution spans +// tens of terabytes, and must cost exactly what the packed one costs. +BOOST_AUTO_TEST_CASE(table_size_is_independent_of_placement) { + const std::size_t n = 1000; + + auto packed = + check_injective_over>( + ids_packed(n)); + auto spread = + check_injective_over>( + ids_dlopened(n)); + BOOST_TEST(packed == spread); + + auto packed_two = check_injective_over>( + ids_packed(n)); + auto spread_two = check_injective_over>( + ids_dlopened(n)); + BOOST_TEST(packed_two == spread_two); +} + +// `LoadPercent = 100` asks for exactly one slot per type id. +BOOST_AUTO_TEST_CASE(minimal_perfect_hash_can_be_exactly_minimal) { + for (auto n : {std::size_t(17), std::size_t(256), std::size_t(1000)}) { + BOOST_TEST_CONTEXT("n = " << n) { + auto slots = check_injective_over< + mph_minimal_registry, pol::minimal_perfect_hash<2, 100>>( + ids_diluted(n)); + BOOST_TEST(slots == n); + } + } +} + +// The default leaves a little slack, and spends it: at most one slot in twenty +// more than there are type ids. +BOOST_AUTO_TEST_CASE(minimal_perfect_hash_is_near_minimal) { + const std::size_t n = 1000; + auto slots = + check_injective_over>( + ids_diluted(n)); + // `slots = ceil(n * 100 / LoadPercent)`, the policy's own formula. + BOOST_TEST(slots == (n * 100 + 94) / 95); +} + +// two_level_hash rounds up to a power of two, so between one and two slots per +// type id - and exactly one when the count is already a power of two. +BOOST_AUTO_TEST_CASE(two_level_hash_table_is_a_power_of_two) { + for (auto n : {std::size_t(17), std::size_t(256), std::size_t(1000)}) { + BOOST_TEST_CONTEXT("n = " << n) { + auto slots = + check_injective_over>( + ids_diluted(n)); + BOOST_TEST((slots & (slots - 1)) == 0u); + BOOST_TEST(slots >= n); + BOOST_TEST(slots < n * 2); + } + } +} + +// A type id may be registered by more than one module, so the same one can +// appear in several class views. The table is over the *distinct* ids. +BOOST_AUTO_TEST_CASE(repeated_type_ids_are_not_collisions) { + auto ids = ids_diluted(64); + auto doubled = ids; + doubled.insert(doubled.end(), ids.begin(), ids.end()); + + fake_context ctx; + + for (const auto& id : doubled) { + ctx.views.push_back(fake_class_view{&id, &id + 1}); + } + + using fn = pol::minimal_perfect_hash<>::fn; + fn::initialize(ctx, std::tuple<>{}); + auto [low, high] = fn::hash_range(); + BOOST_TEST(high - low + 1 <= ids.size() + ids.size() / 20 + 1); + + std::set seen; + + for (auto id : ids) { + BOOST_TEST(seen.insert(fn::hash(id)).second); + } + + fn::finalize(std::tuple<>{}); +} + +// finalize() releases what initialize() allocated. Re-initializing afterwards +// has to work, which is what `initialize()` does on every call. +BOOST_AUTO_TEST_CASE(initialize_after_finalize) { + using fn = pol::minimal_perfect_hash<>::fn; + + for (int round = 0; round != 3; ++round) { + auto ids = ids_diluted(128 + std::size_t(round) * 8); + auto ctx = context_over(ids); + fn::initialize(ctx, std::tuple<>{}); + BOOST_TEST(fn::hash_range().second + 1 >= ids.size()); + fn::finalize(std::tuple<>{}); + BOOST_TEST(fn::hash_range().second == 0u); + } +} diff --git a/test/test_initialize_context.cpp b/test/test_initialize_context.cpp index f6980860..2b9e1be8 100644 --- a/test/test_initialize_context.cpp +++ b/test/test_initialize_context.cpp @@ -94,8 +94,11 @@ auto poke_animal(Animal&) -> std::string { } BOOST_AUTO_TEST_CASE(the_class_range_is_an_input_range) { - BOOST_OPENMETHOD_REGISTER(use_classes); - BOOST_OPENMETHOD_REGISTER(poke::override); + // Function-local statics: register on first pass through this + // declaration, not before main. BOOST_OPENMETHOD_REGISTER is now + // `inline`, which is illegal at block scope, so it's spelled out here. + static use_classes BOOST_OPENMETHOD_GENSYM; + static poke::override BOOST_OPENMETHOD_GENSYM; initialize(); diff --git a/test/test_initialize_dropped_class.cpp b/test/test_initialize_dropped_class.cpp index 3ef579ad..78e86c99 100644 --- a/test/test_initialize_dropped_class.cpp +++ b/test/test_initialize_dropped_class.cpp @@ -186,9 +186,12 @@ auto entry_for() { BOOST_AUTO_TEST_CASE_TEMPLATE( dropped_class_does_not_keep_its_vptr, Registry, registries<__COUNTER__>) { - BOOST_OPENMETHOD_REGISTER(use_classes); - BOOST_OPENMETHOD_REGISTER( - typename speak::template override>); + // Function-local statics: register on first pass through this + // declaration, not before main. BOOST_OPENMETHOD_REGISTER is now + // `inline`, which is illegal at block scope, so it's spelled out here. + static use_classes BOOST_OPENMETHOD_GENSYM; + static typename speak::template override> + BOOST_OPENMETHOD_GENSYM; decltype(entry_for()) tiger_entry; diff --git a/test/test_initialize_policy_state_scope.cpp b/test/test_initialize_policy_state_scope.cpp index 155bba89..23e120aa 100644 --- a/test/test_initialize_policy_state_scope.cpp +++ b/test/test_initialize_policy_state_scope.cpp @@ -105,8 +105,11 @@ auto poke_animal(Animal&) -> std::string { } BOOST_AUTO_TEST_CASE(config_state_survives_a_failed_initialize) { - BOOST_OPENMETHOD_REGISTER(use_classes); - BOOST_OPENMETHOD_REGISTER(poke::override); + // Function-local statics: register on first pass through this + // declaration, not before main. BOOST_OPENMETHOD_REGISTER is now + // `inline`, which is illegal at block scope, so it's spelled out here. + static use_classes BOOST_OPENMETHOD_GENSYM; + static poke::override BOOST_OPENMETHOD_GENSYM; initialize(); BOOST_TEST(test_reg::state().generation == 1); diff --git a/test/test_initialize_transaction.cpp b/test/test_initialize_transaction.cpp index 29b8b078..df925fdf 100644 --- a/test/test_initialize_transaction.cpp +++ b/test/test_initialize_transaction.cpp @@ -159,14 +159,18 @@ BOOST_AUTO_TEST_CASE_TEMPLATE( // Dog is registered here with Animal as its direct base, although it // really derives from Carnivore. The missing edge is added between the two // initializes, below. - BOOST_OPENMETHOD_REGISTER(use_classes); - BOOST_OPENMETHOD_REGISTER(use_classes); - BOOST_OPENMETHOD_REGISTER( - typename poke::template override>); - BOOST_OPENMETHOD_REGISTER( - typename poke::template override>); - BOOST_OPENMETHOD_REGISTER( - typename poke::template override>); + // Function-local statics: register on first pass through this + // declaration, not before main. BOOST_OPENMETHOD_REGISTER is now + // `inline`, which is illegal at block scope, so it's spelled out here. + static use_classes + BOOST_OPENMETHOD_GENSYM; + static use_classes BOOST_OPENMETHOD_GENSYM; + static typename poke::template override> + BOOST_OPENMETHOD_GENSYM; + static typename poke::template override> + BOOST_OPENMETHOD_GENSYM; + static typename poke::template override> + BOOST_OPENMETHOD_GENSYM; Dog dog; Cat cat; @@ -192,8 +196,11 @@ BOOST_AUTO_TEST_CASE_TEMPLATE( // computed from; the Carnivore edge inserts `poke_carnivore` between // `poke_dog` and `poke_animal`, changing what `next` resolves // to. The final initialize below observes both. - BOOST_OPENMETHOD_REGISTER(use_classes); - BOOST_OPENMETHOD_REGISTER(use_classes); + // BOOST_OPENMETHOD_REGISTER is now `inline`, which is illegal at block + // scope, so it's spelled out here (same function-local-static reasoning + // as above). + static use_classes BOOST_OPENMETHOD_GENSYM; + static use_classes BOOST_OPENMETHOD_GENSYM; explosive::armed = true; BOOST_CHECK_THROW(initialize(), std::runtime_error); @@ -247,11 +254,14 @@ BOOST_AUTO_TEST_CASE_TEMPLATE( typename Registry::registry_type>; using vptr_state = typename snapshot::vptr_state; - BOOST_OPENMETHOD_REGISTER(use_classes); - BOOST_OPENMETHOD_REGISTER( - typename poke::template override>); - BOOST_OPENMETHOD_REGISTER( - typename poke::template override>); + // Function-local statics: register on first pass through this + // declaration, not before main. BOOST_OPENMETHOD_REGISTER is now + // `inline`, which is illegal at block scope, so it's spelled out here. + static use_classes BOOST_OPENMETHOD_GENSYM; + static typename poke::template override> + BOOST_OPENMETHOD_GENSYM; + static typename poke::template override> + BOOST_OPENMETHOD_GENSYM; Dog dog; auto& st = Registry::state(); @@ -344,9 +354,13 @@ BOOST_AUTO_TEST_CASE(a_throwing_trace_does_not_commit) { using Registry = tracing_registry<__COUNTER__>; using vptr_state = typename snapshot::vptr_state; - BOOST_OPENMETHOD_REGISTER(use_classes); - BOOST_OPENMETHOD_REGISTER(poke::override>); - BOOST_OPENMETHOD_REGISTER(poke::override>); + // Function-local statics: register on first pass through this + // declaration, not before main. BOOST_OPENMETHOD_REGISTER is now + // `inline`, which is illegal at block scope, so it's spelled out here. + static use_classes BOOST_OPENMETHOD_GENSYM; + static poke::override> + BOOST_OPENMETHOD_GENSYM; + static poke::override> BOOST_OPENMETHOD_GENSYM; Dog dog; auto& st = Registry::state(); @@ -383,9 +397,13 @@ BOOST_AUTO_TEST_CASE(a_throwing_report_does_not_commit) { using Registry = tracing_registry<__COUNTER__>; using vptr_state = typename snapshot::vptr_state; - BOOST_OPENMETHOD_REGISTER(use_classes); - BOOST_OPENMETHOD_REGISTER(poke::override>); - BOOST_OPENMETHOD_REGISTER(poke::override>); + // Function-local statics: register on first pass through this + // declaration, not before main. BOOST_OPENMETHOD_REGISTER is now + // `inline`, which is illegal at block scope, so it's spelled out here. + static use_classes BOOST_OPENMETHOD_GENSYM; + static poke::override> + BOOST_OPENMETHOD_GENSYM; + static poke::override> BOOST_OPENMETHOD_GENSYM; Dog dog; auto& st = Registry::state(); @@ -423,11 +441,14 @@ BOOST_AUTO_TEST_CASE_TEMPLATE( typename Registry::registry_type>; using vptr_state = typename snapshot::vptr_state; - BOOST_OPENMETHOD_REGISTER(use_classes); - BOOST_OPENMETHOD_REGISTER( - typename poke::template override>); - BOOST_OPENMETHOD_REGISTER( - typename poke::template override>); + // Function-local statics: register on first pass through this + // declaration, not before main. BOOST_OPENMETHOD_REGISTER is now + // `inline`, which is illegal at block scope, so it's spelled out here. + static use_classes BOOST_OPENMETHOD_GENSYM; + static typename poke::template override> + BOOST_OPENMETHOD_GENSYM; + static typename poke::template override> + BOOST_OPENMETHOD_GENSYM; Dog dog; auto& st = Registry::state(); diff --git a/test/test_member_overrider.cpp b/test/test_member_overrider.cpp new file mode 100644 index 00000000..7500dc5f --- /dev/null +++ b/test/test_member_overrider.cpp @@ -0,0 +1,139 @@ +// 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 + +#include +#include + +#include "test_util.hpp" + +#define BOOST_TEST_MODULE member_overrider +#include + +using namespace boost::openmethod; + +// ---------------------------------------------------------------------------- +// BOOST_OPENMETHOD_OVERRIDE_FN at namespace scope, on already-existing free +// functions - the case it serves independently of member overriders. + +namespace free_fn { + +struct Animal { + virtual ~Animal() = default; +}; + +struct Dog : Animal {}; +struct Cat : Animal {}; + +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat); + +BOOST_OPENMETHOD(speak, (virtual_ptr), std::string); + +auto speak_dog(virtual_ptr) -> std::string { + return "bark"; +} + +auto speak_cat(virtual_ptr) -> std::string { + return "meow"; +} + +BOOST_OPENMETHOD_OVERRIDE_FN( + speak, (virtual_ptr), std::string, speak_dog, speak_cat); + +} // namespace free_fn + +// ---------------------------------------------------------------------------- +// Member overriders: static member functions of a class other than the one +// dispatched on, given access to its private state with no `friend` +// declaration - the motivating case documented (via the `friend` idiom it +// replaces) in doc/modules/ROOT/pages/friends.adoc. + +namespace member { + +struct Employee { + virtual ~Employee() = default; +}; + +struct Salesman : Employee { + double sales = 0.0; +}; + +BOOST_OPENMETHOD_TEST_CLASSES(Employee, Salesman); + +// Only a reference to it appears in the method's parameter list, so the +// forward declaration is enough - Payroll is not an Employee, and is not +// dispatched on. +class Payroll; + +BOOST_OPENMETHOD(pay, (Payroll & payroll, virtual_ptr), double); + +class Payroll { + public: + double balance() const { + return balance_; + } + + private: + double balance_ = 1'000'000.0; + + void update_balance(double amount) { + // A private member, reachable from the overriders below only because + // they are members of Payroll too - no friend declaration needed. + balance_ += amount; + } + + static auto pay_employee(Payroll& payroll, virtual_ptr) + -> double { + double amount = 5000.0; + payroll.update_balance(-amount); + return amount; + } + + static auto pay_salesman(Payroll& payroll, virtual_ptr emp) + -> double { + double base = pay_employee(payroll, emp); + double commission = emp->sales * 0.05; + payroll.update_balance(-commission); + return base + commission; + } + + // One registrar, naming both member overriders of `pay` for this class - + // override is already variadic, so this is not one line per + // overrider. + BOOST_OPENMETHOD_OVERRIDE_FN( + pay, (Payroll & payroll, virtual_ptr), double, + &Payroll::pay_employee, &Payroll::pay_salesman); +}; + +} // namespace member + +BOOST_AUTO_TEST_CASE(override_fn_namespace_scope) { + initialize(); + + using namespace free_fn; + + Dog snoopy; + Cat felix; + BOOST_TEST(speak(virtual_ptr(snoopy)) == "bark"); + BOOST_TEST(speak(virtual_ptr(felix)) == "meow"); +} + +BOOST_AUTO_TEST_CASE(member_overrider_private_access) { + initialize(); + + using namespace member; + + Payroll payroll; + Employee bill; + Salesman bob; + bob.sales = 100'000.0; + + BOOST_TEST(pay(payroll, virtual_ptr(bill)) == 5000.0); + BOOST_TEST(pay(payroll, virtual_ptr(bob)) == 10000.0); + BOOST_TEST(payroll.balance() == 985000.0); +} + +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_policies.cpp b/test/test_policies.cpp index af35b617..2392de05 100644 --- a/test/test_policies.cpp +++ b/test/test_policies.cpp @@ -10,6 +10,9 @@ #include #include +#include +#include +#include #include "test_util.hpp" @@ -80,3 +83,34 @@ static_assert(!has_initialize< static_assert(has_initialize< fast_perfect_hash::fn, registry1::compiler>, std::tuple<>>); + +// The alternative `type_hash` policies conform to the same blueprint. Each is a +// class template, so name a specialization; the defaults are what a user who +// does not tune them gets. +static_assert(has_initialize< + minimal_perfect_hash<>::fn, + registry1::compiler>, std::tuple<>>); +static_assert(has_initialize< + two_level_hash<>::fn, + registry1::compiler>, std::tuple<>>); +#if BOOST_OPENMETHOD_HAS_PEXT +static_assert(has_initialize< + minimal_cover_hash<>::fn, + registry1::compiler>, std::tuple<>>); +#endif + +// All four are interchangeable: each derives from the `type_hash` category, so +// `with` replaces whichever one a registry already has, in place, rather than +// appending a second - which would leave `vptr_vector` reading the wrong state. +static_assert(std::is_base_of_v); +static_assert(std::is_base_of_v>); +static_assert(std::is_base_of_v>); +static_assert(std::is_base_of_v>); +static_assert(std::is_same_v< + default_registry::with>::policy, + minimal_perfect_hash<>::fn< + default_registry::with>>>); +static_assert( + mp11::mp_size::value == + mp11::mp_size< + default_registry::with>::policy_list>::value); diff --git a/test/test_shared_virtual_ptr_dispatch.cpp b/test/test_shared_virtual_ptr_dispatch.cpp index f59ff804..f5de2838 100644 --- a/test/test_shared_virtual_ptr_dispatch.cpp +++ b/test/test_shared_virtual_ptr_dispatch.cpp @@ -23,16 +23,19 @@ struct BOOST_OPENMETHOD_ID(fight); BOOST_AUTO_TEST_CASE_TEMPLATE( test_virtual_ptr_dispatch, Registry, policy_types<__COUNTER__>) { - BOOST_OPENMETHOD_REGISTER( - use_classes); + // Function-local static: registers on first pass through this + // declaration, not before main. BOOST_OPENMETHOD_REGISTER is now + // `inline`, which is illegal at block scope, so it's spelled out here. + static use_classes + BOOST_OPENMETHOD_GENSYM; using poke = method< BOOST_OPENMETHOD_ID(poke), auto(shared_virtual_ptr)->std::string, Registry>; - BOOST_OPENMETHOD_REGISTER( - typename poke::template override< - poke_bear>>); + static typename poke::template override< + poke_bear>> + BOOST_OPENMETHOD_GENSYM; using fight = method< BOOST_OPENMETHOD_ID(fight), @@ -43,11 +46,11 @@ BOOST_AUTO_TEST_CASE_TEMPLATE( ->std::string, Registry>; - BOOST_OPENMETHOD_REGISTER( - typename fight::template override, - shared_virtual_ptr, - shared_virtual_ptr>>); + static typename fight::template override, + shared_virtual_ptr, + shared_virtual_ptr>> + BOOST_OPENMETHOD_GENSYM; initialize(); diff --git a/test/test_slot_allocator.cpp b/test/test_slot_allocator.cpp index e8c8cbc7..887abda2 100644 --- a/test/test_slot_allocator.cpp +++ b/test/test_slot_allocator.cpp @@ -126,10 +126,14 @@ BOOST_AUTO_TEST_CASE(test_use_classes_linear) { struct registry : test_registry_<__COUNTER__> {}; - BOOST_OPENMETHOD_CLASSES(Base, D1, D2, D3, registry); - BOOST_OPENMETHOD_CLASSES(D2, D3, registry); - BOOST_OPENMETHOD_CLASSES(D3, D4, registry); - BOOST_OPENMETHOD_CLASSES(D4, D5, D3, registry); + // Function-local statics: register on first pass through this + // declaration, not before main. BOOST_OPENMETHOD_CLASSES expands through + // BOOST_OPENMETHOD_REGISTER, now `inline` - illegal at block scope - so + // it's bypassed here in favor of its own expansion, spelled out by hand. + static use_classes BOOST_OPENMETHOD_GENSYM; + static use_classes BOOST_OPENMETHOD_GENSYM; + static use_classes BOOST_OPENMETHOD_GENSYM; + static use_classes BOOST_OPENMETHOD_GENSYM; auto comp = initialize(); @@ -179,9 +183,13 @@ BOOST_AUTO_TEST_CASE(test_use_classes_derived_before_base) { struct registry : test_registry_<__COUNTER__> {}; - BOOST_OPENMETHOD_CLASSES(D3, D4, registry); - BOOST_OPENMETHOD_CLASSES(D4, D5, registry); - BOOST_OPENMETHOD_CLASSES(Base, D1, D2, D3, registry); + // Function-local statics: register on first pass through this + // declaration, not before main. BOOST_OPENMETHOD_CLASSES expands through + // BOOST_OPENMETHOD_REGISTER, now `inline` - illegal at block scope - so + // it's bypassed here in favor of its own expansion, spelled out by hand. + static use_classes BOOST_OPENMETHOD_GENSYM; + static use_classes BOOST_OPENMETHOD_GENSYM; + static use_classes BOOST_OPENMETHOD_GENSYM; auto comp = initialize(); @@ -213,7 +221,11 @@ BOOST_AUTO_TEST_CASE(test_use_classes_derived_before_base) { BOOST_AUTO_TEST_CASE(test_use_classes_diamond) { using test_registry = test_registry_<__COUNTER__>; using namespace diamond; - BOOST_OPENMETHOD_REGISTER(use_classes); + // Function-local static: registers on first pass through this + // declaration, not before main. BOOST_OPENMETHOD_REGISTER is now + // `inline`, which is illegal at block scope, so it's spelled out here. + static use_classes + BOOST_OPENMETHOD_GENSYM; std::vector actual, expected; @@ -1691,7 +1703,10 @@ BOOST_AUTO_TEST_CASE(test_finalize_clears_vptr_vector) { }; struct B : A {}; - BOOST_OPENMETHOD_REGISTER(use_classes); + // Function-local static: registers on first pass through this + // declaration, not before main. BOOST_OPENMETHOD_REGISTER is now + // `inline`, which is illegal at block scope, so it's spelled out here. + static use_classes BOOST_OPENMETHOD_GENSYM; (void)method)->void, test_registry>::fn; initialize(); @@ -1720,8 +1735,11 @@ BOOST_AUTO_TEST_CASE(test_registries_do_not_share_vptr_state) { }; struct B : A {}; - BOOST_OPENMETHOD_REGISTER(use_classes); - BOOST_OPENMETHOD_REGISTER(use_classes); + // Function-local statics: register on first pass through this + // declaration, not before main. BOOST_OPENMETHOD_REGISTER is now + // `inline`, which is illegal at block scope, so it's spelled out here. + static use_classes BOOST_OPENMETHOD_GENSYM; + static use_classes BOOST_OPENMETHOD_GENSYM; (void)method)->void, registry1>::fn; (void)method)->void, registry2>::fn; diff --git a/test/test_virtual_ptr_dispatch.cpp b/test/test_virtual_ptr_dispatch.cpp index eb3a8455..dc3e0041 100644 --- a/test/test_virtual_ptr_dispatch.cpp +++ b/test/test_virtual_ptr_dispatch.cpp @@ -22,15 +22,18 @@ struct BOOST_OPENMETHOD_ID(fight); BOOST_AUTO_TEST_CASE_TEMPLATE( test_virtual_ptr_dispatch, Registry, policy_types<__COUNTER__>) { - BOOST_OPENMETHOD_REGISTER( - use_classes); + // Function-local static: registers on first pass through this + // declaration, not before main. BOOST_OPENMETHOD_REGISTER is now + // `inline`, which is illegal at block scope, so it's spelled out here. + static use_classes + BOOST_OPENMETHOD_GENSYM; using poke = method< BOOST_OPENMETHOD_ID(poke), auto(virtual_ptr)->std::string, Registry>; - BOOST_OPENMETHOD_REGISTER( - typename poke::template override< - poke_bear>>); + static typename poke::template override< + poke_bear>> + BOOST_OPENMETHOD_GENSYM; using fight = method< BOOST_OPENMETHOD_ID(fight), @@ -39,10 +42,10 @@ BOOST_AUTO_TEST_CASE_TEMPLATE( virtual_ptr) ->std::string, Registry>; - BOOST_OPENMETHOD_REGISTER( - typename fight::template override, virtual_ptr, - virtual_ptr>>); + static typename fight::template override, virtual_ptr, + virtual_ptr>> + BOOST_OPENMETHOD_GENSYM; initialize(); diff --git a/test/test_virtual_ptr_final.cpp b/test/test_virtual_ptr_final.cpp index 29be3c63..2fb206b3 100644 --- a/test/test_virtual_ptr_final.cpp +++ b/test/test_virtual_ptr_final.cpp @@ -21,14 +21,17 @@ struct BOOST_OPENMETHOD_ID(poke); BOOST_AUTO_TEST_CASE_TEMPLATE( test_virtual_ptr, Registry, policy_types<__COUNTER__>) { - BOOST_OPENMETHOD_REGISTER( - use_classes); + // Function-local static: registers on first pass through this + // declaration, not before main. BOOST_OPENMETHOD_REGISTER is now + // `inline`, which is illegal at block scope, so it's spelled out here. + static use_classes + BOOST_OPENMETHOD_GENSYM; using poke = method< BOOST_OPENMETHOD_ID(poke), auto(virtual_ptr)->std::string, Registry>; - BOOST_OPENMETHOD_REGISTER( - typename poke::template override< - poke_bear>>); + static typename poke::template override< + poke_bear>> + BOOST_OPENMETHOD_GENSYM; initialize(); diff --git a/test/test_virtual_ptr_value_semantics.cpp b/test/test_virtual_ptr_value_semantics.cpp index 1eb6269e..4ff6992a 100644 --- a/test/test_virtual_ptr_value_semantics.cpp +++ b/test/test_virtual_ptr_value_semantics.cpp @@ -276,7 +276,11 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(indirect_virtual_ptr, Registry, test_policies) { // namespace scan cannot see it: the registration is by hand under every // standard, like the one in the header. struct Cat : Animal {}; - BOOST_OPENMETHOD_CLASSES(Animal, Cat, Registry); + // Function-local static: registers on first pass through this + // declaration, not before main. BOOST_OPENMETHOD_CLASSES expands through + // BOOST_OPENMETHOD_REGISTER, now `inline` - illegal at block scope - so + // it's bypassed here in favor of its own expansion, spelled out by hand. + static use_classes BOOST_OPENMETHOD_GENSYM; init_test(); diff --git a/test/test_virtual_ptr_value_semantics.hpp b/test/test_virtual_ptr_value_semantics.hpp index b6bf1818..fdab5485 100644 --- a/test/test_virtual_ptr_value_semantics.hpp +++ b/test/test_virtual_ptr_value_semantics.hpp @@ -54,7 +54,10 @@ struct NonPolymorphic {}; template void init_test() { - BOOST_OPENMETHOD_REGISTER(use_classes); + // Function-local static: registers on first pass through this + // declaration, not before main. BOOST_OPENMETHOD_REGISTER is now + // `inline`, which is illegal at block scope, so it's spelled out here. + static use_classes BOOST_OPENMETHOD_GENSYM; struct id; // without following line, no methods, no v-tables (void)&method)->void, Registry>::fn; diff --git a/test/test_weak_virtual_ptr.cpp b/test/test_weak_virtual_ptr.cpp new file mode 100644 index 00000000..65a6a19b --- /dev/null +++ b/test/test_weak_virtual_ptr.cpp @@ -0,0 +1,451 @@ +// 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 + +#define BOOST_TEST_MODULE weak_virtual_ptr +#include + +#include "test_virtual_ptr_value_semantics.hpp" + +#include +#include +#include +#include + +// A weak virtual_ptr is not a virtual_ptr at all: neither a smart one in the +// `IsSmartPtr` sense (no `virtual_traits`, no `rebind`) nor a plain one. +static_assert(!IsSmartPtr, default_registry>); +static_assert(!is_virtual_ptr>); + +// Moves are noexcept, so containers relocate by moving, not copying. +static_assert(std::is_nothrow_move_constructible_v>); +static_assert(std::is_nothrow_move_assignable_v>); + +static_assert(std::is_same_v::element_type, Animal>); +static_assert(std::is_same_v< + decltype(std::declval>().lock()), + shared_virtual_ptr>); +static_assert(std::is_same_v< + decltype(std::declval>().pointer()), + const std::weak_ptr&>); + +// Construction is allowed from shared and weak pointers, virtual or not... +static_assert(std::is_constructible_v< + weak_virtual_ptr, shared_virtual_ptr>); +static_assert( + std::is_constructible_v, shared_virtual_ptr>); +static_assert( + std::is_constructible_v, weak_virtual_ptr>); +static_assert( + std::is_constructible_v, std::shared_ptr>); +static_assert( + std::is_constructible_v, std::weak_ptr>); +static_assert(std::is_constructible_v< + weak_virtual_ptr, shared_virtual_ptr>); + +// ...but not from a plain pointer, reference or virtual_ptr, nor from a +// different class or a const object... +static_assert(!std::is_constructible_v, Animal&>); +static_assert(!std::is_constructible_v, Animal*>); +static_assert( + !std::is_constructible_v, virtual_ptr>); +static_assert( + !std::is_constructible_v, shared_virtual_ptr>); +static_assert(!std::is_constructible_v< + weak_virtual_ptr, shared_virtual_ptr>); +static_assert( + !std::is_constructible_v< + weak_virtual_ptr, std::shared_ptr>); +static_assert(!std::is_constructible_v< + weak_virtual_ptr, std::weak_ptr>); + +// ...and a weak virtual_ptr converts to nothing but another weak virtual_ptr. +static_assert( + !std::is_constructible_v, weak_virtual_ptr>); +static_assert( + !std::is_assignable_v&, weak_virtual_ptr>); +static_assert(!std::is_constructible_v< + shared_virtual_ptr, weak_virtual_ptr>); +static_assert(!std::is_assignable_v< + shared_virtual_ptr&, weak_virtual_ptr>); +static_assert(!std::is_constructible_v< + std::shared_ptr, weak_virtual_ptr>); + +BOOST_AUTO_TEST_CASE_TEMPLATE( + weak_virtual_ptr_from_shared_virtual_ptr, Registry, test_policies) { + init_test(); + + auto snoopy = std::make_shared(); + shared_virtual_ptr shared(snoopy); + + weak_virtual_ptr weak(shared); + BOOST_TEST(!weak.expired()); + BOOST_TEST(weak.use_count() == 2); + BOOST_TEST(weak.vptr() == Registry::template static_vptr); + BOOST_TEST(weak.pointer().lock() == snoopy); + + { + auto locked = weak.lock(); + static_assert(std::is_same_v< + decltype(locked), shared_virtual_ptr>); + BOOST_TEST(locked.get() == snoopy.get()); + BOOST_TEST(locked.vptr() == Registry::template static_vptr); + BOOST_TEST(weak.use_count() == 3); + } + + BOOST_TEST(weak.use_count() == 2); + + shared = nullptr; + snoopy.reset(); + BOOST_TEST(weak.expired()); + BOOST_TEST(weak.use_count() == 0); + + auto locked = weak.lock(); + BOOST_TEST(locked.get() == nullptr); + BOOST_TEST(locked.vptr() == nullptr); + + weak.reset(); + BOOST_TEST(weak.expired()); + BOOST_TEST(weak.vptr() == nullptr); +} + +BOOST_AUTO_TEST_CASE_TEMPLATE( + weak_virtual_ptr_from_std_pointers, Registry, test_policies) { + init_test(); + + auto felix = std::make_shared(); + + { + weak_virtual_ptr weak(felix); + BOOST_TEST(weak.vptr() == Registry::template static_vptr); + BOOST_TEST(weak.lock().get() == felix.get()); + } + + { + std::weak_ptr std_weak = felix; + weak_virtual_ptr weak(std_weak); + BOOST_TEST(weak.vptr() == Registry::template static_vptr); + BOOST_TEST(weak.lock().get() == felix.get()); + } + + { + // an expired std::weak_ptr yields an expired weak virtual_ptr + std::weak_ptr std_weak; + { + auto dead = std::make_shared(); + std_weak = dead; + } + + weak_virtual_ptr weak(std_weak); + BOOST_TEST(weak.expired()); + BOOST_TEST(weak.vptr() == nullptr); + BOOST_TEST(weak.lock().get() == nullptr); + BOOST_TEST(weak.use_count() == 0); + } + + { + // an expired source of the same class keeps its control block, so it + // keeps its owner identity. Across a class conversion that is up to + // the standard library: libstdc++ shares ownership, as + // [util.smartptr.weak.const] requires of a source that is expired but + // not empty; libc++ locks first, and an expired source then yields an + // empty weak pointer. + std::weak_ptr std_weak; + { + auto dead = std::make_shared(); + std_weak = dead; + } + + weak_virtual_ptr weak(std_weak); + BOOST_TEST(weak.expired()); + BOOST_TEST(weak.vptr() == nullptr); + BOOST_TEST(!weak.pointer().owner_before(std_weak)); + BOOST_TEST(!std_weak.owner_before(weak.pointer())); + } + + { + // an empty std::shared_ptr yields an empty weak virtual_ptr + weak_virtual_ptr weak{std::shared_ptr()}; + BOOST_TEST(weak.expired()); + BOOST_TEST(weak.vptr() == nullptr); + } +} + +BOOST_AUTO_TEST_CASE_TEMPLATE( + weak_virtual_ptr_default_and_nullptr, Registry, test_policies) { + init_test(); + + { + weak_virtual_ptr weak; + BOOST_TEST(weak.expired()); + BOOST_TEST(weak.use_count() == 0); + BOOST_TEST(weak.vptr() == nullptr); + BOOST_TEST(weak.lock().get() == nullptr); + } + + { + weak_virtual_ptr weak(nullptr); + BOOST_TEST(weak.expired()); + BOOST_TEST(weak.vptr() == nullptr); + } + + { + auto snoopy = std::make_shared(); + weak_virtual_ptr weak(snoopy); + BOOST_TEST(!weak.expired()); + + weak = nullptr; + BOOST_TEST(weak.expired()); + BOOST_TEST(weak.vptr() == nullptr); + } +} + +BOOST_AUTO_TEST_CASE_TEMPLATE( + weak_virtual_ptr_assign, Registry, test_policies) { + init_test(); + + auto snoopy = std::make_shared(); + auto felix = std::make_shared(); + weak_virtual_ptr weak; + + weak = shared_virtual_ptr(snoopy); + BOOST_TEST(weak.lock().get() == snoopy.get()); + BOOST_TEST(weak.vptr() == Registry::template static_vptr); + + weak = shared_virtual_ptr(felix); + BOOST_TEST(weak.lock().get() == felix.get()); + BOOST_TEST(weak.vptr() == Registry::template static_vptr); + + weak = snoopy; + BOOST_TEST(weak.lock().get() == snoopy.get()); + BOOST_TEST(weak.vptr() == Registry::template static_vptr); + + weak = std::weak_ptr(felix); + BOOST_TEST(weak.lock().get() == felix.get()); + BOOST_TEST(weak.vptr() == Registry::template static_vptr); + + weak_virtual_ptr weak_dog(snoopy); + weak = weak_dog; + BOOST_TEST(weak.lock().get() == snoopy.get()); + BOOST_TEST(weak.vptr() == Registry::template static_vptr); + BOOST_TEST(!weak_dog.expired()); + + weak = *&weak; // self-assignment + BOOST_TEST(weak.lock().get() == snoopy.get()); + BOOST_TEST(weak.vptr() == Registry::template static_vptr); +} + +BOOST_AUTO_TEST_CASE_TEMPLATE( + weak_virtual_ptr_copy_move, Registry, test_policies) { + init_test(); + + auto snoopy = std::make_shared(); + weak_virtual_ptr weak_dog(snoopy); + + { + weak_virtual_ptr copy(weak_dog); + BOOST_TEST(copy.lock().get() == snoopy.get()); + BOOST_TEST(copy.vptr() == Registry::template static_vptr); + BOOST_TEST(weak_dog.lock().get() == snoopy.get()); + } + + { + // upcast, copying + weak_virtual_ptr base(weak_dog); + BOOST_TEST(base.lock().get() == snoopy.get()); + BOOST_TEST(base.vptr() == Registry::template static_vptr); + BOOST_TEST(weak_dog.lock().get() == snoopy.get()); + } + + { + weak_virtual_ptr source(snoopy); + weak_virtual_ptr moved(std::move(source)); + BOOST_TEST(moved.lock().get() == snoopy.get()); + BOOST_TEST(moved.vptr() == Registry::template static_vptr); + BOOST_TEST(source.expired()); + BOOST_TEST(source.vptr() == nullptr); + } + + { + // upcast, moving + weak_virtual_ptr source(snoopy); + weak_virtual_ptr moved(std::move(source)); + BOOST_TEST(moved.lock().get() == snoopy.get()); + BOOST_TEST(moved.vptr() == Registry::template static_vptr); + BOOST_TEST(source.expired()); + BOOST_TEST(source.vptr() == nullptr); + } + + { + weak_virtual_ptr source(snoopy); + weak_virtual_ptr moved; + moved = std::move(source); + BOOST_TEST(moved.lock().get() == snoopy.get()); + BOOST_TEST(moved.vptr() == Registry::template static_vptr); + BOOST_TEST(source.expired()); + BOOST_TEST(source.vptr() == nullptr); + } + + { + auto felix = std::make_shared(); + weak_virtual_ptr weak_cat(felix); + weak_virtual_ptr weak_animal(weak_dog); + weak_cat.swap(weak_animal); + BOOST_TEST(weak_cat.lock().get() == snoopy.get()); + BOOST_TEST(weak_cat.vptr() == Registry::template static_vptr); + BOOST_TEST(weak_animal.lock().get() == felix.get()); + BOOST_TEST(weak_animal.vptr() == Registry::template static_vptr); + } +} + +// `std::owner_less` is generic only in libstdc++; MSVC's and libc++'s +// have overloads for `std::shared_ptr` and `std::weak_ptr` alone, so a +// container keyed on owner identity carries its own comparator. +struct owner_less { + template + auto operator()(const Left& left, const Right& right) const -> bool { + return left.owner_before(right); + } +}; + +BOOST_AUTO_TEST_CASE_TEMPLATE( + weak_virtual_ptr_owner_before, Registry, test_policies) { + init_test(); + + auto snoopy = std::make_shared(); + auto felix = std::make_shared(); + shared_virtual_ptr shared_dog(snoopy); + weak_virtual_ptr weak_dog(shared_dog); + weak_virtual_ptr weak_dog_too(snoopy); + weak_virtual_ptr weak_cat(felix); + + // same owner, against a shared and a weak virtual_ptr, to another class + BOOST_TEST(!weak_dog.owner_before(shared_dog)); + BOOST_TEST(!weak_dog.owner_before(weak_dog_too)); + BOOST_TEST(!weak_dog_too.owner_before(weak_dog)); + + // different owners: a strict weak ordering, the same as std::weak_ptr's + BOOST_TEST( + weak_dog.owner_before(weak_cat) != weak_cat.owner_before(weak_dog)); + BOOST_TEST( + weak_dog.owner_before(weak_cat) == + std::weak_ptr(snoopy).owner_before(felix)); + + std::set, owner_less> observers; + observers.insert(weak_dog); + observers.insert(weak_cat); + observers.insert(weak_virtual_ptr(snoopy)); // same owner + BOOST_TEST(observers.size() == 2u); + BOOST_TEST(observers.count(weak_dog) == 1u); + BOOST_TEST(observers.count(weak_cat) == 1u); +} + +BOOST_AUTO_TEST_CASE_TEMPLATE( + weak_virtual_ptr_non_polymorphic, Registry, test_policies) { + // The v-table pointer is copied from the shared_virtual_ptr, so the class + // need not be polymorphic; only the vptr lookup would require that. + // + // Function-local static: registers on first pass through this + // declaration, not before main. BOOST_OPENMETHOD_REGISTER is now + // `inline`, which is illegal at block scope, so it's spelled out here. + static use_classes BOOST_OPENMETHOD_GENSYM; + init_test(); + + auto shared = make_shared_virtual(); + weak_virtual_ptr weak(shared); + BOOST_TEST(weak.vptr() == Registry::template static_vptr); + BOOST_TEST(weak.lock().get() == shared.get()); + BOOST_TEST( + weak.lock().vptr() == Registry::template static_vptr); +} + +struct BOOST_OPENMETHOD_ID(poke); + +// Namespace-scope templates: gcc before 13 does not accept the static member +// functions of a local class as template arguments (no linkage). +template +auto poke_dog(shared_virtual_ptr) -> std::string { + return "bark"; +} + +template +auto poke_cat(shared_virtual_ptr) -> std::string { + return "hiss"; +} + +struct BOOST_OPENMETHOD_ID(observe); + +// A weak_virtual_ptr is not a virtual_ptr, so it can be an ordinary, non-virtual +// parameter of a method: the object it tracks is not dispatched on. +template +auto observe_dog( + virtual_ptr, weak_virtual_ptr other) + -> std::string { + return other.expired() ? "dog sees nobody" : "dog sees somebody"; +} + +template +auto observe_cat( + virtual_ptr, weak_virtual_ptr other) + -> std::string { + return other.expired() ? "cat sees nobody" : "cat sees somebody"; +} + +BOOST_AUTO_TEST_CASE_TEMPLATE( + weak_virtual_ptr_non_virtual_parameter, Registry, test_policies) { + using observe = method< + BOOST_OPENMETHOD_ID(observe), + auto(virtual_ptr, weak_virtual_ptr) + ->std::string, + Registry>; + + // Function-local static: registers on first pass through this + // declaration, not before main. BOOST_OPENMETHOD_REGISTER is now + // `inline`, which is illegal at block scope, so it's spelled out here. + static typename observe::template override< + observe_dog, observe_cat> + BOOST_OPENMETHOD_GENSYM; + + init_test(); + + auto snoopy = std::make_shared(); + auto felix = std::make_shared(); + virtual_ptr dog(*snoopy); + virtual_ptr cat(*felix); + weak_virtual_ptr weak_dog(snoopy); + weak_virtual_ptr weak_cat(felix); + + BOOST_TEST(observe::fn(dog, weak_cat) == "dog sees somebody"); + BOOST_TEST(observe::fn(cat, weak_dog) == "cat sees somebody"); + + felix.reset(); + BOOST_TEST(observe::fn(dog, weak_cat) == "dog sees nobody"); +} + +BOOST_AUTO_TEST_CASE_TEMPLATE( + weak_virtual_ptr_dispatch, Registry, test_policies) { + using poke = method< + BOOST_OPENMETHOD_ID(poke), + auto(shared_virtual_ptr)->std::string, Registry>; + + // Function-local static: registers on first pass through this + // declaration, not before main. BOOST_OPENMETHOD_REGISTER is now + // `inline`, which is illegal at block scope, so it's spelled out here. + static + typename poke::template override, poke_cat> + BOOST_OPENMETHOD_GENSYM; + + init_test(); + + auto snoopy = std::make_shared(); + auto felix = std::make_shared(); + weak_virtual_ptr weak_dog(snoopy); + weak_virtual_ptr weak_cat(felix); + + // lock, then dispatch + BOOST_TEST(poke::fn(weak_dog.lock()) == "bark"); + BOOST_TEST(poke::fn(weak_cat.lock()) == "hiss"); +}