diff --git a/CLAUDE.md b/CLAUDE.md index 0bb72468..98131495 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -149,6 +149,12 @@ The project uses clang-format with an LLVM-based style: - `AllowShortFunctionsOnASingleLine: false` - No short blocks, if statements, or loops on single lines +### Disassembly +Always show disassembly in **Intel syntax**, never AT&T (the toolchain's default +here). Add the flag to the invocation before pasting any output: +- `objdump -dC -M intel --no-show-raw-insn` +- `gcc -S -masm=intel` / `clang -S -masm=intel` + ### Compiler Requirements Tests require these C++17 features (checked by Boost.Build): - auto nontype template params @@ -159,131 +165,116 @@ Tests require these C++17 features (checked by Boost.Build): - structured bindings - ``, ``, `` headers +### Documentation (AsciiDoc) + +Prose lives in `doc/modules/ROOT/pages/*.adoc`; explanations belong there, not in comments inside +the example sources under `doc/modules/ROOT/examples/`, which are pulled into the rendered page +verbatim through `include::example$file.cpp[tag=content]`. Pages hard-wrap at ~79 columns and use +`cpp:name[]` for API names that have a reference page. + +**Render the docs; do not just eyeball the `.adoc`.** `doc/build_antora.sh` (~2 min, writes the +gitignored `doc/html/`) is the only way to catch markup that is silently mis-parsed — asciidoctor +emits no warning for it. + +**Side-by-side comparisons use a table with AsciiDoc cells.** The house shape is +`[cols="1,1"]` + `|===` with a header row (`interop_any.adoc`, `registries_and_policies.adoc`, +`shared_libraries.adoc`). A cell holding a *block* - a code listing, a nested list - must be +introduced with `a|`, not `|`. The `a` makes the cell content parsed as AsciiDoc; without it the +`[source,...]` / `----` markup renders literally, and asciidoctor says nothing. +`interop_type_erasure.adoc` compares two dispatch sequences that way: + +``` +[cols="1,1"] +|=== +| `openmethod_vptr` | `virtual_any` + +a| +[source,asm] +---- +mov rax, qword ptr [rdi + 32] +---- + +a| +[source,asm] +---- +jmp qword ptr [rax + 8*rcx] +---- + +|=== +``` + +`|` starts a new cell, so cell content containing one must escape it as `\|`. Quick structural +check before rendering - both counts must be even, and every `[source,X]` must be followed by +`----`: + +```bash +grep -c '^----$' doc/modules/ROOT/pages/.adoc +grep -c '^|===$' doc/modules/ROOT/pages/.adoc +``` + +**The backtick-apostrophe trap**: never write a possessive right after a code span. Asciidoctor +parses ``` `any`'s ``` as `` ` `` + `any` + the **`` `' `` curly-apostrophe shorthand**, which +consumes the *closing* backtick; the opening one is then left unmatched and pairs with the next +backtick in the same paragraph. Two things break at once — a literal `` ` `` appears in the output, +and the following code span loses its `` formatting: + +``` +source: is part of the `any`'s type - whereas the `typeid_of`-based dispatch above +rendered: is part of the any's type - whereas the `typeid_of-based dispatch above +``` + +Reword instead: "the reference types of the `any`", "separate from that of `default_registry`". +``{apos}`` also works and matches the house style (`shared_libraries.adoc` uses ``{empty}`` for +plurals: ``` `virtual_ptr`{empty}s ```), but rewording is safer and reads better. Before building: + +```bash +grep -rn "\`'" doc/modules/ROOT/pages/*.adoc # must return nothing +``` + +After building, no stray backticks should survive outside code blocks — +`grep -n '\`' doc/html/openmethod/.html` should only hit backticks inside C++ comments. + ## Common Development Patterns ### Working with Shared Libraries / DLL Support -**Overview**: The library supports shared library usage across modules by sharing the registry's -state through an export/import decoration of a single symbol. On Windows (and Cygwin) the decoration -is dllexport/dllimport; on ELF it is `visibility("default")` on the export side. In the common case -off Windows the decoration can be omitted entirely — the state then has ordinary external linkage -and is shared by the dynamic linker — but that only works if the program is *not* built with hidden -visibility. Under `-fvisibility=hidden` (e.g. the Boost super-project's `BoostRoot.cmake`) an -implicitly instantiated `st` is a COMDAT that gets internalized to a per-module local symbol, so -the export/import macros must be used on ELF too (they emit a single strong, default-visibility -explicit instantiation that the other modules import). - -**One shared state variable**: All of a registry's mutable state — the class/method/overrider -lists *and* every stateful policy's `state` (held together in the `registry_state_type::policies` -tuple) — lives in a single variable, `registry_state::st` of type -`detail::registry_state_type`. A registry reaches it through `Registry::state()`. Sharing -a registry across a DLL boundary therefore means sharing this one symbol. - -`registry_state` (in `boost::openmethod`) is a deliberately thin, function-free class whose only -member is the static `st`. It is kept *separate* from `registry_state_type` (the struct holding the -actual fields, in `detail`) because MSVC only honors `dllexport`/`dllimport` on a *whole-class* -explicit instantiation — not on a variable template (clients silently get a private copy) nor on a -static-data-member instantiation (error C2720) — and dllexporting `registry_state_type` directly -would also decorate its member functions and the policies' nested `state` types, which MSVC rejects -(error C2513). A one-member, function-free class is the only shape MSVC will export as a whole and -import via `extern template`. - -**Mechanism — `extern template` / explicit instantiation**: the shared symbol is -`registry_state::st`, where `registry_type` is the `registry` -*base* of the registry struct (that is what `registry::state()` uses — never key on the derived -struct). The owning module compiles, in exactly one TU, an exported explicit instantiation -definition; clients compile an imported explicit instantiation declaration, so they reference -the owner's symbol instead of instantiating their own copy: -```cpp -// owner (one TU): -template struct BOOST_SYMBOL_EXPORT registry_state; -// clients: -extern template struct BOOST_SYMBOL_IMPORT registry_state; -``` -`BOOST_SYMBOL_EXPORT`/`BOOST_SYMBOL_IMPORT` are dllexport/dllimport on Windows and -`visibility("default")` / empty on ELF, so the same two lines serve both platforms. This is no -longer guarded by `_WIN32`: on ELF the pair is what makes the state shareable under hidden -visibility. - -**Registries are structs, not aliases — do not "simplify" this**: `default_registry` (and the -documented custom-registry pattern) is deliberately a *struct deriving from* `registry`, -never a type alias. The short struct name keeps mangled/linker names short for everything keyed on -the registry (methods, virtual_ptrs, `static_vptr`, registrars...); an alias would expand to the -full policy list in all of those symbols. This is also why the `::registry_type` spelling in the -explicit instantiations above cannot be avoided: an explicit instantiation instantiates exactly the -specialization written, so making `registry_state` work would require -`default_registry` to *be* its base (an alias) — rejected for the mangled-name reason. Only the -shared state symbol carries the full policy list, which is accepted. - -**Usage**: three macros, each taking the registry as an argument, so the same three serve -`default_registry`, `indirect_registry` and user-defined registries. Everything they emit is fully -qualified, so callers never open `namespace boost::openmethod`: +A registry's entire mutable state - the class/method/overrider lists plus every stateful policy's +`state` - lives in one variable, `registry_state::st`. Sharing a registry across modules +means sharing that one symbol. Three macros do it, each taking the registry as an argument and +emitting fully qualified names, so callers never open `namespace boost::openmethod`: + ```cpp -// header, every TU of a client module -BOOST_OPENMETHOD_IMPORT_REGISTRY(boost::openmethod::default_registry); -// header, every TU of the owning module -BOOST_OPENMETHOD_EXPORT_REGISTRY(boost::openmethod::default_registry); -// exactly one .cpp of the owning module -BOOST_OPENMETHOD_INSTANTIATE_REGISTRY(boost::openmethod::default_registry); +BOOST_OPENMETHOD_IMPORT_REGISTRY(R); // header, every TU of a client module +BOOST_OPENMETHOD_EXPORT_REGISTRY(R); // header, every TU of the owning module +BOOST_OPENMETHOD_INSTANTIATE_REGISTRY(R); // exactly one .cpp of the owning module ``` -The owning module uses two: `EXPORT` in the shared header, `INSTANTIATE` once. Note -`::registry_type` inside the expansions: the state is keyed on the `registry<...>` base, never the -derived struct. - -**Why three macros and not raw incantations** — the underlying explicit instantiations are not -portable, and each spelling fails on one platform while compiling silently on the other. The -macros branch on `BOOST_HAS_DECLSPEC`: -- *declspec platforms* (Windows/Cygwin/MinGW): `__declspec(dllexport)` and `extern` are - incompatible on an explicit instantiation — MSVC emits `warning C4910` and, with warnings-as- - errors, fails. It is also unnecessary there, visibility not being a PE concept. So `EXPORT` - expands to nothing (`static_assert(true)`, to swallow the `;`) and `INSTANTIATE` carries the - `dllexport`. -- *ELF and Mach-O*: the attribute must be on the **declaration**; repeating it on the definition is - `error: type attributes ignored after type is already defined [-Werror=attributes]` on GCC, which - clang accepts silently. So `EXPORT` carries it and `INSTANTIATE` carries none. - -**`EXPORT` is load-bearing on ELF**, not documentation: a TU of the owning module with neither -`EXPORT` nor `INSTANTIATE` instantiates the state implicitly, and under `-fvisibility=hidden` that -copy is module-local. ELF merges COMDATs at the *most restrictive* visibility, so the merged symbol -becomes local, the module exports nothing, and clients fail to link with an undefined reference to -`registry_state<...>::st`. `test/implicit_shared_libraries/custom_registry/lib2.cpp` is a second -owner TU kept solely to guard that path. Placement within the instantiating TU does not matter -(verified with `readelf` under `-fvisibility=hidden`). - -**Methods need no decoration**: method objects are *consolidated* across modules at `initialize()` -time, not shared via a single symbol, so `BOOST_OPENMETHOD(...)` takes no declspec argument. - -See `doc/modules/ROOT/examples/shared_libs/` — one self-contained example per subdirectory -(`implicit_linking/`, `dynamic_loading/`, `indirect_vptr/`), each with its own `animals.hpp`, -`main.cpp` and `extensions.cpp` so every file spells out its export/import macro unconditionally — -plus `test/dynamic_loading/` and `test/implicit_shared_libraries/` for the tests. - -**Dynamic Loading Test** (`test/dynamic_loading/`): verifies that the registry state is a single -shared symbol across modules. `registry_state_id()` (in `registry.hpp`) returns the registry-state -address (`test_registry::id()`); `main.cpp`'s `same_ids()` compares two such addresses (registry vs. -method, registry vs. overrider) and asserts they are identical. (Policy state lives inside -`registry_state_type`, so the registry-state address is the one shared symbol.) Files: -- `registry.hpp` — defines `test_registry` (indirect iff `BOOST_OPENMETHOD_DEFAULT_REGISTRY` is defined on the command line), then emits `BOOST_OPENMETHOD_{EXPORT,IMPORT}_REGISTRY(test_registry)` according to whether the module compiles with `EXPORT_REGISTRY`; defines `registry_state_id()` -- `classes.hpp` — `Animal`/`Dog`/`Cat` definitions (marked `BOOST_SYMBOL_VISIBLE` so their RTTI stays - default-visibility under the hidden-visibility CMake variant below) + `make_dog`/`make_cat` -- `method.hpp` — declares the `speak`/`meet` methods (no declspec arguments) -- `shared_overrider.hpp` — one `speak` overrider for `Cat`, included identically by `method.cpp` and - `overrider.cpp` to exercise cross-module overrider deduplication (the same overrider registered by - two modules must not be treated as ambiguous) -- `registry.cpp` — compiled with `EXPORT_REGISTRY`; the shared library that owns and exports the registry state -- `method.cpp` — client (imports the registry state); defines base overriders (including the shared - Cat one), exports C entry points -- `overrider.cpp` — dynamically loaded at runtime; adds a Dog overrider and the shared Cat overrider -- `main.cpp` — links the registry lib, dlopens the method and overrider libs, checks `same_ids`, calls - `initialize()`, tests cross-module dispatch (including the Cat overrider-dedup and class-dedup - regression checks) - -CMake builds five variants: `_default`/`_indirect` (dll-owned state) and `_exereg_default`/ -`_exereg_indirect` (exe-owned state), crossed with the default/indirect registry, plus `_hidden_vis` -(forces `CXX_VISIBILITY_PRESET hidden` on every target to reproduce, on a standalone build, the -configuration where `augment_classes()`'s class-dedup must key on `(type, static_vptr)` rather than -`type` alone). b2's Jamfile only builds the dll-owned default/indirect pair; it does not currently -have a hidden-visibility variant. + +Methods need no decoration: method objects are *consolidated* across modules at `initialize()` +time, not shared through a symbol. + +**Do not hand-write the underlying explicit instantiations.** They are not portable, and each +spelling compiles silently on one platform while failing on the other. On declspec platforms +`__declspec(dllexport)` and `extern` are incompatible on an explicit instantiation (MSVC warning +C4910), so `EXPORT` expands to nothing and `INSTANTIATE` carries the attribute; on ELF and Mach-O +the attribute must be on the *declaration*, and repeating it on the definition is an error on GCC, +so `EXPORT` carries it and `INSTANTIATE` carries none. The macros branch on `BOOST_HAS_DECLSPEC`. + +**`EXPORT` is load-bearing on ELF, not documentation.** Under `-fvisibility=hidden` (e.g. the Boost +super-project's `BoostRoot.cmake`) an owner TU with neither `EXPORT` nor `INSTANTIATE` instantiates +the state implicitly as a COMDAT; ELF merges COMDATs at the *most restrictive* visibility, so the +merged symbol goes module-local, the module exports nothing, and clients fail to link. +`test/implicit_shared_libraries/custom_registry/lib2.cpp` exists solely to guard that path. + +**Registries are structs deriving from `registry`, never aliases - do not "simplify" +this.** The short struct name keeps mangled names short for everything keyed on the registry +(methods, virtual_ptrs, `static_vptr`, registrars); an alias would expand the full policy list into +all of them. That is also why the state is keyed on `Registry::registry_type` - the `registry<...>` +base - and never on the derived struct. `registry_state` is likewise a deliberately thin, +function-free class: that is the only shape MSVC will export whole and import via `extern template`. + +One self-contained example per subdirectory of `doc/modules/ROOT/examples/shared_libs/`; tests in +`test/dynamic_loading/` (whose `registry_state_id()` is compared across modules to prove the state +is a single symbol) and `test/implicit_shared_libraries/`. ### Custom RTTI When `` is unavailable or insufficient, use static_rtti or implement custom RTTI. See `doc/modules/ROOT/examples/custom_rtti/` and policies in `include/boost/openmethod/policies/`. diff --git a/CMakeLists.txt b/CMakeLists.txt index cdd278df..5224a251 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -94,6 +94,17 @@ set( if (BOOST_OPENMETHOD_BUILD_TESTS OR BOOST_OPENMETHOD_MRDOCS_BUILD) list(APPEND BOOST_OPENMETHOD_DEPENDENCIES Boost::smart_ptr) + list(APPEND BOOST_OPENMETHOD_DEPENDENCIES Boost::any) + list(APPEND BOOST_OPENMETHOD_DEPENDENCIES Boost::type_erasure) + # ... but for its headers only. Boost.TypeErasure's compiled part is one + # file, dynamic_binding.cpp, which nothing here references and which links + # Boost::thread - so linking Boost::type_erasure builds Boost.Thread and, + # through it, Boost.Container, Boost.Chrono, Boost.date_time and + # Boost.Atomic. Two of those do not build under -Werror on the Windows CI + # toolchains, which took the whole test suite down with them. Consume the + # usage requirements without linking, as test/Jamfile does with . + # ($ expresses this directly, but needs CMake 3.27.) + list(APPEND BOOST_OPENMETHOD_COMPILE_ONLY_DEPENDENCIES Boost::type_erasure) endif() foreach (BOOST_OPENMETHOD_DEPENDENCY ${BOOST_OPENMETHOD_DEPENDENCIES}) @@ -176,7 +187,25 @@ set_property(GLOBAL PROPERTY USE_FOLDERS ON) source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR}/include/boost PREFIX "" FILES ${BOOST_OPENMETHOD_HEADERS}) function(boost_openmethod_setup_properties target) - target_link_libraries(${target} INTERFACE ${BOOST_OPENMETHOD_DEPENDENCIES}) + set(link_dependencies ${BOOST_OPENMETHOD_DEPENDENCIES}) + + # $ is exactly "the usage requirements, without the link". + # Being a generator expression, it does not care whether the dependency's + # target has been defined yet - which the super-project, driving this from + # BOOST_INCLUDE_LIBRARIES, does not guarantee. Anything that inspects the + # target at configure time does care, and breaks when the super-project + # happens to configure openmethod first. + # + # It needs CMake 3.27, well past the floor declared here. Below that, link + # normally: slower, and it builds Boost.Thread and friends, but correct. + if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.27) + foreach (dependency ${BOOST_OPENMETHOD_COMPILE_ONLY_DEPENDENCIES}) + list(REMOVE_ITEM link_dependencies ${dependency}) + list(APPEND link_dependencies $) + endforeach() + endif() + + target_link_libraries(${target} INTERFACE ${link_dependencies}) endfunction() add_library(boost_openmethod INTERFACE) diff --git a/doc/modules/ROOT/examples/type_erasure.cpp b/doc/modules/ROOT/examples/type_erasure.cpp new file mode 100644 index 00000000..f9a8c532 --- /dev/null +++ b/doc/modules/ROOT/examples/type_erasure.cpp @@ -0,0 +1,73 @@ +// Copyright (c) 2018-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) + +// clang-format off + +// tag::content[] +#include +#include + +#include +#include +#include +#include + +#include +#include + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +// `relaxed` implies `typeid_<>`, which dispatch relies on. +using Concept = boost::mpl::vector, te::relaxed>; +using erased = te::any; + +struct Dog { + std::string name; +}; + +// The owning `any`, `any`, is the common base of the types the +// `any` may bind. An overrider registers the type it names as a class +// derived from it. +BOOST_OPENMETHOD(name, (virtual_), std::string); + +// An overrider takes the bound value... +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { + return name; +} + +// ...or the `any` itself, which makes it a catch-all. +BOOST_OPENMETHOD_OVERRIDE(name, (const erased& value), std::string) { + return te::is_empty(value) ? "nothing" : "something else"; +} + +BOOST_OPENMETHOD(weigh, (virtual_), int); + +BOOST_OPENMETHOD_OVERRIDE(weigh, (const int& value), int) { + return value; +} + +#include + +int main() { + initialize(); + + const erased spot(Dog{"Spot"}); + const erased felix(std::string("Felix the cat")); + const erased answer(42); + + std::cout << name(spot) << "\n"; // Spot the dog + std::cout << name(felix) << "\n"; // Felix the cat + + // `int` is registered - `weigh`'s overrider names it - but has no + // `name` overrider of its own, so the catch-all applies. + std::cout << weigh(answer) << "\n"; // 42 + std::cout << name(answer) << "\n"; // something else +} +// end::content[] diff --git a/doc/modules/ROOT/examples/type_erasure_concept.cpp b/doc/modules/ROOT/examples/type_erasure_concept.cpp new file mode 100644 index 00000000..a6664cfb --- /dev/null +++ b/doc/modules/ROOT/examples/type_erasure_concept.cpp @@ -0,0 +1,55 @@ +// Copyright (c) 2018-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) + +// clang-format off + +// tag::content[] +#include +#include + +#include +#include +#include +#include + +#include +#include + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +struct Dog { + std::string name; +}; + +struct Dispatchable + : boost::mpl::vector< + te::copy_constructible<>, te::relaxed, + openmethod_vptr> {}; + +using erased = te::any; + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const erased& value), std::string) { + return te::is_empty(value) ? "nothing" : "something else"; +} + +#include + +int main() { + initialize(); + + const erased spot(Dog{"Spot"}); + const erased answer(42); + + std::cout << name(spot) << "\n"; // Spot the dog + std::cout << name(answer) << "\n"; // something else +} +// end::content[] diff --git a/doc/modules/ROOT/examples/type_erasure_ref.cpp b/doc/modules/ROOT/examples/type_erasure_ref.cpp new file mode 100644 index 00000000..f07f2d52 --- /dev/null +++ b/doc/modules/ROOT/examples/type_erasure_ref.cpp @@ -0,0 +1,57 @@ +// Copyright (c) 2018-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) + +// clang-format off + +// tag::content[] +#include +#include + +#include +#include +#include + +#include +#include + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +using Concept = boost::mpl::vector, te::relaxed>; +using erased_ref = te::any; + +struct Dog { + std::string name; +}; + +// An any reference is a cheap handle; it is passed by value. +BOOST_OPENMETHOD(poke, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(poke, (Dog& dog), std::string) { + dog.name += "!"; + return dog.name; +} + +BOOST_OPENMETHOD_OVERRIDE(poke, (int& value), std::string) { + ++value; + return "poked"; +} + +#include + +int main() { + initialize(); + + Dog snoopy{"Snoopy"}; + int count = 41; + + // mutations reach the referents + std::cout << poke(erased_ref(snoopy)) << "\n"; // Snoopy! + std::cout << snoopy.name << "\n"; // Snoopy! + + std::cout << poke(erased_ref(count)) << "\n"; // poked + std::cout << count << "\n"; // 42 +} +// end::content[] diff --git a/doc/modules/ROOT/examples/type_erasure_virtual_any.cpp b/doc/modules/ROOT/examples/type_erasure_virtual_any.cpp new file mode 100644 index 00000000..55440262 --- /dev/null +++ b/doc/modules/ROOT/examples/type_erasure_virtual_any.cpp @@ -0,0 +1,58 @@ +// Copyright (c) 2018-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) + +// clang-format off + +// tag::content[] +#include +#include + +#include +#include +#include + +#include +#include + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +using Concept = boost::mpl::vector, te::relaxed>; +using erased = te::any; + +struct Dog { + std::string name; +}; + +BOOST_OPENMETHOD(name, (const virtual_any&), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { + return name; +} + +#include + +int main() { + initialize(); + + // from a value: the v-table pointer is set statically + virtual_any rex = Dog{"Rex"}; + std::cout << (rex.vptr() == default_registry::static_vptr) << "\n"; // 1 + + // from an `any`: one lookup, here, and none in the calls below + erased spot_any(Dog{"Spot"}); + virtual_any spot = spot_any; + + std::cout << name(rex) << "\n"; // Rex the dog + std::cout << name(spot) << "\n"; // Spot the dog + + spot = std::string("Felix the cat"); + std::cout << name(spot) << "\n"; // Felix the cat +} +// end::content[] diff --git a/doc/modules/ROOT/examples/virtual_any.cpp b/doc/modules/ROOT/examples/virtual_any.cpp new file mode 100644 index 00000000..0fa8a66b --- /dev/null +++ b/doc/modules/ROOT/examples/virtual_any.cpp @@ -0,0 +1,69 @@ +// Copyright (c) 2018-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) + +// clang-format off + +// tag::content[] +#include +#include +#include + +#include +#include + +using namespace boost::openmethod; + +struct Dog { + std::string name; +}; + +// `std::any` is the common base of the types it may contain. An overrider +// registers the type it names as a class derived from it. +BOOST_OPENMETHOD(name, (virtual_), std::string); + +// An overrider takes the contained value... +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { + return name; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const int& value), std::string) { + return std::to_string(value) + " the integer"; +} + +// ...or the `any` itself, which makes it a catch-all. +BOOST_OPENMETHOD_OVERRIDE(name, (const std::any&), std::string) { + return "something else"; +} + +BOOST_OPENMETHOD(weigh, (virtual_), float); + +BOOST_OPENMETHOD_OVERRIDE(weigh, (const float& value), float) { + return value; +} + +#include + +int main() { + initialize(); + + std::any spot = Dog{"Spot"}; + std::any felix = std::string("Felix the cat"); + std::any answer = 42; + std::any pi = 3.14f; + + std::cout << name(spot) << "\n"; // Spot the dog + std::cout << name(felix) << "\n"; // Felix the cat + std::cout << name(answer) << "\n"; // 42 the integer + + // `float` is registered - `weigh`'s overrider names it - but has no + // `name` overrider of its own, so the catch-all applies. + std::cout << weigh(pi) << "\n"; // 3.14 + std::cout << name(pi) << "\n"; // something else +} +// end::content[] diff --git a/doc/modules/ROOT/examples/virtual_ptr_alt/1/virtual_ptr_alt.cpp b/doc/modules/ROOT/examples/virtual_ptr_alt/1/virtual_ptr_alt.cpp index 56bba033..460d9bc1 100644 --- a/doc/modules/ROOT/examples/virtual_ptr_alt/1/virtual_ptr_alt.cpp +++ b/doc/modules/ROOT/examples/virtual_ptr_alt/1/virtual_ptr_alt.cpp @@ -28,7 +28,6 @@ struct Times : Node { const Node& left; const Node& right; }; -// tag::content[] #include #include diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index edaebaa3..8b036b60 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -13,6 +13,8 @@ ** xref:custom_rtti.adoc[Custom RTTI] ** xref:error_handling.adoc[Error Handling] ** xref:virtual_ptr_alt.adoc[Virtual Pointer Alternatives] +** xref:interop_any.adoc[Interoperation with `any`] +** xref:interop_type_erasure.adoc[Interoperation with Boost.TypeErasure] ** xref:shared_libraries.adoc[Shared Libraries] * Reference ** xref:ref_headers.adoc[Headers] diff --git a/doc/modules/ROOT/pages/interop_any.adoc b/doc/modules/ROOT/pages/interop_any.adoc new file mode 100644 index 00000000..194b338c --- /dev/null +++ b/doc/modules/ROOT/pages/interop_any.adoc @@ -0,0 +1,152 @@ + +[#interop_any] +## Interoperation with `any` + +OpenMethod can take `any` (both the `std` and `boost` flavors) as virtual +arguments, and dispatch on the type of the contained value. For this purpose, it +regards the types as "deriving" from `any`. + +#### Requirements + +Dispatch keys on the `std::type_info` returned by `any::type()`, so the +registry's `rtti` policy must be cpp:std_rtti[], or a policy derived from it. +`default_registry` and `indirect_registry` both qualify. A registry with, say, +cpp:static_rtti[] identifies classes by a different kind of `type_id`, and would +look up the wrong v-table; the requirement is enforced with a `static_assert`. + +#### `std::any` + +Support is provided by ``. It is not +included by ``, so it must be included explicitly. + +Dispatch works on classes known to a registry. The types the `any` may contain +are registered automatically: naming a type as the parameter of an overrider +registers it as a class derived from `std::any`; storing a value in a +cpp:virtual_std_any[] (see below) registers its type as well. A type that is +never named in one of these ways is not registered and cannot be dispatched on +- not even by a catch-all overrider: a call with such a value in the `any` is a +cpp:missing_class[] error - see xref:error_handling.adoc[Error Handling]. + +The `any` is passed like any other virtual argument that is not a +`virtual_ptr`: wrapped in `virtual_`, as described in +xref:virtual_ptr_alt.adoc[Alternatives to virtual_ptr]. Overriders receive the +_contained_ value by reference. An overrider may also take the `any` itself; +such an overrider is a catch-all, applying to any registered contained type +that has no more specific overrider - in the example below, `float`, which the +`weigh` overrider registers, but which has no `name` overrider of its own: + +[source,c++] +---- +include::example$virtual_any.cpp[tag=content] +---- + +#### Mixing with ordinary virtual parameters + +A multi-method can take any combination of ordinary virtual parameters and +virtual `any` in the same call: + +```c++ +BOOST_OPENMETHOD( + meet, (virtual_, virtual_ptr), std::string); + +BOOST_OPENMETHOD_OVERRIDE( + meet, (const Dog& dog, virtual_ptr), std::string) { + return dog.name + " meets a cat"; +} +``` + +#### Reference categories + +All three reference categories are supported, and they determine what the +overriders may take: + +[%autowidth,cols="1,2"] +|=== +| Method parameter | Overrider parameter + +| `virtual_` +| `const Dog&`, `Dog` + +| `virtual_` +| `Dog&`, `const Dog&`, `Dog` + +| `virtual_` +| `Dog&&`, `const Dog&`, `Dog` +|=== + +The mutable lvalue reference is the awkward one. +xref:reference:BOOST_OPENMETHOD_OVERRIDE.adoc[BOOST_OPENMETHOD_OVERRIDE] locates +the method by checking that the overrider's parameters can be passed to the +method's forwarder, and `Dog&` does not convert to `std::any&`. A temporary +`std::any` binds to `const std::any&` and to `std::any&&`, which is why the +other two categories can use the macro; nothing binds to a mutable lvalue +reference. Those overriders are registered with the core API instead - the +primitive the macro itself expands to: + +```c++ +using bump_method = + BOOST_OPENMETHOD_TYPE(bump, (virtual_), std::string); + +auto bump_dog(Dog& dog) -> std::string { + dog.name += " Jr."; + return dog.name; +} + +BOOST_OPENMETHOD_REGISTER(bump_method::override); +``` + +#### `virtual_std_any` + +Every call above looks the v-table up in a hash table, keyed on the type the +`any` contains. cpp:virtual_std_any[] - an alias for `virtual_any` - +removes that cost: it bundles an `any` with the v-table pointer for the value +inside it, acquiring it once, on construction, and maintaining it across +assignment and `emplace`. It is similar to cpp:virtual_ptr[], except that it +_owns_ the object: the `any` is held by value. + +The pointer comes from a lookup when the `virtual_std_any` is built from an +existing `any`, and from a static variable - no lookup at all - when it is built +from a value, or by `emplace`, since the type is then known at compile time. +Building from a value, or `emplace`, also _registers_ the type, like naming it +in an overrider does; so does assigning a value. + +That makes it worthwhile when the same value is dispatched on repeatedly. Its +usefulness is limited, though, by the fact that the wrapper is not what an +overrider receives: an overrider takes the contained value, as before, so it +cannot pass the `virtual_std_any` on to another method and save the lookup +there. Only a catch-all overrider, which takes `const virtual_std_any&`, gets +it. + +A `virtual_std_any` method parameter must be a reference - passing it by value +would copy the `any`, and the value inside it, on every call. The three +categories, and the limitation on the mutable one, are as above. + +For the same reason that a `virtual_std_any` caches what a plain `any` does not, +cpp:final_virtual_ptr[] is _deleted_ for `std::any`: it would silently produce +the v-table of the `any` root class rather than the one for the contained value. + +A `virtual_std_any` cannot itself be wrapped in a cpp:virtual_ptr[]: it is +already a wide type, and the result would be a wide pointer whose v-table is the +contained value's, but whose static type is the `virtual_std_any` - which is +deliberately not a registered class. The combination is rejected at compile +time, cpp:final_virtual_ptr[] included. Pass it by reference instead. + +#### `boost::any` + +`boost::any` is supported as well, by +``, with cpp:virtual_boost_any[] - the +exact counterpart of cpp:virtual_std_any[]. The two root classes are distinct, +so `std::any` and `boost::any` may be used in the same program, and with the +same registry. + +cpp:virtual_any[] itself is generic: it can serve any type with an `any`-like +interface, given cpp:virtual_traits[] specializations for its reference types. +`virtual_any` registers the types it stores; a specialization that wants +overriders to register the types they name, like the `std::any` and +`boost::any` ones do, plants the registration in its `cast` and `vptr` +functions. Boost.TypeErasure's `any` is supported on the same model - see +xref:interop_type_erasure.adoc[Interoperation with Boost.TypeErasure]. + +### Acknowledgment + +This interop is based on a design contributed by Steven Watanabe. diff --git a/doc/modules/ROOT/pages/interop_type_erasure.adoc b/doc/modules/ROOT/pages/interop_type_erasure.adoc new file mode 100644 index 00000000..0a5e2601 --- /dev/null +++ b/doc/modules/ROOT/pages/interop_type_erasure.adoc @@ -0,0 +1,235 @@ + +[#interop_type_erasure] +## Interoperation with Boost.TypeErasure + +Methods can take +link:https://www.boost.org/doc/libs/release/doc/html/boost_typeerasure.html[Boost.TypeErasure] +``any``s as virtual parameters, in the same manner as for a plain ``any``s - see +xref:interop_any.adoc[Interoperation with `any`]. + +Support is provided by ``. It +is not included by ``, so it must be included explicitly. + +#### Requirements + +Dispatch resolves on the type returned by `boost::type_erasure::typeid_of`, so +the ``any``'s concept must include `boost::type_erasure::typeid_<>`. + +`typeid_of` returns a `std::type_info`, so the registry's `rtti` policy must be +cpp:std_rtti[], or a policy derived from it. `default_registry` and +`indirect_registry` both qualify. A registry with, say, cpp:static_rtti[] +identifies classes by a different kind of `type_id`, and would look up the wrong +v-table; the requirement is enforced with a `static_assert`. The +cpp:openmethod_vptr[] concept takes the v-table pointer from the ``any``'s own +dispatch table and never calls `typeid_of`, so it does not require `std_rtti`. + +The types an `any` may bind to are registered automatically: naming a type as +the parameter of an overrider registers it as a class derived from the owning +`any` - the root class for the Concept; storing a value in a cpp:virtual_any[] +(see below) registers its type as well. ``any``s with different Concepts have +distinct roots. A type that is never named in one of these ways is not +registered and cannot be dispatched on - not even by a catch-all overrider: a +call with such a value bound to the ``any`` is a cpp:missing_class[] error - +see xref:error_handling.adoc[Error Handling]. + +The `any` is wrapped in `virtual_` in the method parameters; overriders receive +the _bound_ value - or, for a catch-all overrider, the `any` itself. In the +example below, `int` is registered by the `weigh` overrider, and, having no +`name` overrider of its own, is dispatched by `name` to the catch-all: + +[source,c++] +---- +include::example$type_erasure.cpp[tag=content] +---- + +#### Reference categories + +The `any` is passed by reference, in any of the three categories - +passing it by value would copy the bound value on every call, and is rejected +at compile time. The category determines what the overriders may take: + +[%autowidth,cols="1,2"] +|=== +| Method parameter | Overrider parameter + +| `virtual_&>` +| `const Dog&`, `Dog` + +| `virtual_&>` +| `Dog&`, `const Dog&`, `Dog` + +| `virtual_&&>` +| `Dog&&`, `const Dog&`, `Dog` +|=== + +The mutable lvalue reference has the same limitation as `virtual_`: +xref:reference:BOOST_OPENMETHOD_OVERRIDE.adoc[BOOST_OPENMETHOD_OVERRIDE] +cannot locate the method, because nothing binds a temporary `any` to a mutable +lvalue reference - see the +xref:interop_any.adoc#interop_any[explanation there]. Those overriders are +registered with the core API instead: + +```c++ +using bump_method = + BOOST_OPENMETHOD_TYPE(bump, (virtual_&>), std::string); + +auto bump_dog(Dog& dog) -> std::string { + dog.name += " Jr."; + return dog.name; +} + +BOOST_OPENMETHOD_REGISTER(bump_method::override); +``` + +`boost::type_erasure::any_cast` has no rvalue overload, so, in the rvalue +case, moving the value out of the `any` is performed by the interop code +itself: an overrider taking `Dog&&` receives the bound value ready to be moved +from, and the `any` still owns the moved-from object afterwards. + +#### `any` references + +Boost.TypeErasure also has non-owning ``any``'s, `any` and +`any`, which hold a _reference_ to a value stored +elsewhere. They are cheap, two-word handles, and, unlike an owning `any`, they +are passed by value - the idiomatic way to use them as parameters. Modifications +made through a mutable any reference reach the referent: + +[source,c++] +---- +include::example$type_erasure_ref.cpp[tag=content] +---- + +Dispatch is on the type _bound at construction_ of the any reference - never on +the C++ RTTI dynamic type of the referent. The rvalue-reference placeholder +(`_self&&`), and placeholders other than `_self`, are not supported. + +For the same reason as for `std::any`, cpp:final_virtual_ptr[] is _deleted_ +for `type_erasure::any`: it would silently produce the v-table of the root +class rather than the one for the bound value. + +#### `virtual_any` + +Every call above looks the v-table up in a hash table, keyed on the type the +`any` binds. cpp:virtual_any[] removes that cost: it bundles an `any` with the +v-table pointer for the value bound to it, acquiring it once, on construction, +and maintaining it across assignment and `emplace`. It is to an `any` what +cpp:virtual_ptr[] is to a pointer, except that it _owns_ the object: the `any` +is held by value. Building it from a value, or `emplace`, also _registers_ the +type, like naming it in an overrider does; so does assigning a value. The +``any``'s concept must include `relaxed` - for the default constructor and +assignment - and `copy_constructible<>` for copies. + +[source,c++] +---- +include::example$type_erasure_virtual_any.cpp[tag=content] +---- + +The wrapper is passed by reference, in any of the three categories, and the +overriders receive the _bound_ value, exactly as for `virtual_&>` - or the wrapper itself, for a catch-all overrider. It works +with `type_erasure::any` for the same reason it works with `std::any` and +`boost::any` - the interop specializes cpp:virtual_traits[] for the reference +types of the `any`. See xref:interop_any.adoc[Interoperation with `any`] for +the details. + +#### The `openmethod_vptr` concept + +cpp:openmethod_vptr[] is an alternative to `virtual_any`, pursuing the same +goal - constant-time access to the v-table pointer - from the other side. +Instead of wrapping the `any` from the outside, it puts the v-table pointer +inside the dispatch table of the `any` itself, making every `any` on it +intrinsically polymorphic, and leaving the call sites unchanged. The two are +mutually exclusive: an `any` that carries the concept cannot be wrapped in a +`virtual_any`, because the hook returns the v-table pointer by value, and an +indirect registry cannot store that; wrapping one is rejected at compile time. +`virtual_any` is to an `any` what cpp:virtual_ptr[] is to a pointer, while +`openmethod_vptr` is to an `any` what cpp:inplace_vptr_base[] is to a class - +see xref:virtual_ptr_alt.adoc[Alternatives to virtual_ptr]. + +Constant-time is not the same as free, though, and the two do not cost the +same. Clang compiles a call on the x64 architecture to the following - symbol +names are shortened for readability. + +[%autowidth,cols="1,1"] +|=== +| `openmethod_vptr` | `virtual_any` + +a| +[source,asm] +---- +push rbx +mov rbx, rdi +mov rax, qword ptr [rdi] +add rdi, 24 +call qword ptr [rax + 16] +mov rcx, qword ptr [rip + fn+88] +mov rdi, rbx +pop rbx +jmp qword ptr [rax + 8*rcx] +---- + +a| +[source,asm] +---- +mov rax, qword ptr [rdi + 32] +mov rcx, qword ptr [rip + fn+88] +jmp qword ptr [rax + 8*rcx] +---- + +|=== + +The concept reaches the v-table pointer through a call on the ``any``'s own +dispatch table - the `call` is `openmethod_vptr::apply`, and the register +shuffling around it is there to keep the argument alive across it. A +`virtual_any` holds the pointer as a member, so acquiring it is the one load at +`[rdi + 32]`. + +What the concept removes, relative to a plain `any`, is therefore the hash of +`type_erasure::typeid_of`, not the call. Only `virtual_any` gets down to a +load, and it pays for that in size - the pointer sits next to the `any` - and +in having to acquire it up front. + +`openmethod_vptr` is implemented via the cpp:boost_openmethod_vptr[] ADL +customization point. + +Binding a value to such an `any` also _registers_ its type, as a class derived +from the owning `any` - the registrar is instantiated along with the +operation, just as naming the type in an overrider instantiates one; the two +registration paths may coexist. + +`openmethod_vptr` must name the Concept it belongs to, which makes the +definition self-referential. That rules out a type alias: the Concept is a +struct deriving from the `mpl::vector`, so it can pass its own name - CRTP, in +effect: + +[source,c++] +---- +include::example$type_erasure_concept.cpp[tag=content] +---- + +The price is coupling: the Concept must be OpenMethod-aware, and the registry +is part of the type of the `any` - whereas the `typeid_of`-based dispatch above +works with any pre-existing Concept containing `typeid_<>`. To use an `any` +with several registries, list the concept several times, once per registry: +`openmethod_vptr`. + +In exchange, the requirement for cpp:std_rtti[] goes away. The v-table pointer +comes from the ``any``'s own dispatch table, so the registry's `rtti` policy is +needed only to identify classes when `initialize()` builds the dispatch tables - +cpp:static_rtti[] is enough. No hashing is involved either: the registry needs +neither a `vptr` policy nor any policy that one depends on, like `type_hash`. +`registry` will do. Note that this does not make the +program RTTI-free: Boost.TypeErasure itself uses `typeid`. + +#### Empty ``any``s + +An empty relaxed `any` reports `typeid(void)`, which is not a registered +class, so dispatching on it is a cpp:missing_class[] error. A catch-all +overrider does not help: dispatch never reaches it. Check with +`boost::type_erasure::is_empty` before calling. With the `openmethod_vptr` +concept, the failure mode differs: calling a concept operation on an empty +relaxed `any` throws `boost::type_erasure::bad_function_call`. + +### Acknowledgment + +This interop is based on a design contributed by Steven Watanabe. diff --git a/doc/modules/ROOT/pages/ref_headers.adoc b/doc/modules/ROOT/pages/ref_headers.adoc index ba911432..03c25e67 100644 --- a/doc/modules/ROOT/pages/ref_headers.adoc +++ b/doc/modules/ROOT/pages/ref_headers.adoc @@ -78,6 +78,30 @@ Provides a `virtual_traits` specialization that makes it possible to use a Provides a `virtual_traits` specialization that makes it possible to use a `boost::intrusive_ptr` in place of a raw pointer or reference in virtual parameters. +[#virtual_any] +### link:{headers-url}/boost/openmethod/interop/virtual_any.hpp[] + +Provides `virtual_any`, a wide `any` that combines an `any`, held by value, with +a pointer to the v-table for the contained value - similar to `virtual_ptr`. + +[#std_any] +### link:{headers-url}/boost/openmethod/interop/std_any.hpp[] + +Provides `virtual_traits` specializations that make it possible to use a +`std::any` in virtual parameters. + +[#boost_any] +### link:{headers-url}/boost/openmethod/interop/boost_any.hpp[] + +Provides `virtual_traits` specializations that make it possible to use a +`boost::any` in virtual parameters. + +[#boost_type_erasure] +### link:{headers-url}/boost/openmethod/interop/boost_type_erasure.hpp[] + +Provides specializations for using a `boost::type_erasure::any` in virtual +parameters. + *The headers below are for advanced use*. ## Pre-Core Headers diff --git a/doc/modules/ROOT/pages/shared_libraries.adoc b/doc/modules/ROOT/pages/shared_libraries.adoc index 97da9bbf..0861c919 100644 --- a/doc/modules/ROOT/pages/shared_libraries.adoc +++ b/doc/modules/ROOT/pages/shared_libraries.adoc @@ -253,8 +253,9 @@ and no translation unit can accidentally disagree: include::{shared}/indirect_vptr/animals.hpp[tag=content] ---- -cpp:indirect_registry[] has its own state, separate from `default_registry`'s, -shared with the same three macros: just name `indirect_registry`, as above. +cpp:indirect_registry[] has its own state, separate from that of +`default_registry`, shared with the same three macros: just name +`indirect_registry`, as above. Here is a program that carries `virtual_ptr`{empty}s across `initialize` calls. It owns the state, so it defines `OWNS_REGISTRY_STATE` and emits the definition: diff --git a/doc/modules/ROOT/pages/virtual_ptr_alt.adoc b/doc/modules/ROOT/pages/virtual_ptr_alt.adoc index b9ceca52..29b45cdb 100644 --- a/doc/modules/ROOT/pages/virtual_ptr_alt.adoc +++ b/doc/modules/ROOT/pages/virtual_ptr_alt.adoc @@ -40,8 +40,9 @@ can be called with the virtual argument (passed by const reference) and a pointer to a registry, and returns a `vptr_type`. If one is found, it is called to acquire the vptr. -`virtual_ptr` honors it as well, when it is constructed from - or assigned - a -reference or a pointer to an object. +This is specific to `virtual_` parameters. `virtual_ptr` does not use the +function - it is an alternative to it, not a client of it - and wrapping an +object that provides one is rejected at compile time. In the following example, we embed the vptr in the object, just like the vptr for native virtual functions. The v-table for a registered class can be found @@ -106,3 +107,7 @@ v-table for the bases, just like what C++ does for its native vptrs. `inplace_vptr_base` and `inplace_vptr_derived` are aliased in `namespace boost::openmethod::aliases`. + +An object that embeds its v-table pointer does not need to be wrapped in a +`virtual_ptr` - the two fill the same goal, fast access to the v-table +pointer - and wrapping one is rejected at compile time. diff --git a/doc/modules/ROOT/snippets/type_erasure.cpp b/doc/modules/ROOT/snippets/type_erasure.cpp new file mode 100644 index 00000000..5e3761a3 --- /dev/null +++ b/doc/modules/ROOT/snippets/type_erasure.cpp @@ -0,0 +1,86 @@ +// Copyright (c) 2018-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 +#include +#include + +#include +#include +#include + +#define BOOST_TEST_MODULE openmethod +#include + +#include "capture.hpp" + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +// tag::classes[] +// `relaxed` implies `typeid_<>`, which dispatch relies on. +using Concept = boost::mpl::vector, te::relaxed>; +using erased = te::any; + +struct Dog { + std::string name; +}; + +// The owning `any`, `any`, is the common base of the types the +// `any` may bind. The types are registered automatically: naming one in +// an overrider - or storing a value in a `virtual_any` - registers it. +// end::classes[] + +// tag::method[] +BOOST_OPENMETHOD(name, (virtual_), std::string); + +// An overrider takes the bound value... +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { + return name; +} + +// ...or the `any` itself, which makes it a catch-all. +BOOST_OPENMETHOD_OVERRIDE(name, (const erased& value), std::string) { + return te::is_empty(value) ? "nothing" : "something else"; +} +// end::method[] + +// `int` is registered because `weigh`'s overrider names it; it has no +// `name` overrider, so the catch-all applies to it. + +BOOST_OPENMETHOD(weigh, (virtual_), int); + +BOOST_OPENMETHOD_OVERRIDE(weigh, (const int& value), int) { + return value; +} + +BOOST_AUTO_TEST_CASE(type_erasure_examples) { + initialize(); + + { + capture_cout cout; + + // tag::dispatch[] + const erased spot(Dog{"Spot"}); + const erased answer(42); + + std::cout << name(spot) << "\n"; // Spot the dog + + // `int` is registered - `weigh`'s overrider names it - but has + // no `name` overrider of its own, so the catch-all applies. + std::cout << name(answer) << "\n"; // something else + // end::dispatch[] + + BOOST_TEST(cout.str() == "Spot the dog\nsomething else\n"); + } +} diff --git a/doc/modules/ROOT/snippets/type_erasure_concept.cpp b/doc/modules/ROOT/snippets/type_erasure_concept.cpp new file mode 100644 index 00000000..5744a284 --- /dev/null +++ b/doc/modules/ROOT/snippets/type_erasure_concept.cpp @@ -0,0 +1,61 @@ +// Copyright (c) 2018-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 +#include + +#include +#include +#include + +#define BOOST_TEST_MODULE openmethod +#include + +#include "capture.hpp" + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +// tag::concept[] +struct Dog { + std::string name; +}; + +// The concept must name the Concept it is part of, so the Concept is +// defined as a struct. +struct Dispatchable : boost::mpl::vector< + te::copy_constructible<>, te::relaxed, + openmethod_vptr> {}; + +using erased = te::any; + +// Binding a value to the `any` registers its type. + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} +// end::concept[] + +BOOST_AUTO_TEST_CASE(type_erasure_concept_example) { + initialize(); + + { + capture_cout cout; + + // tag::dispatch[] + const erased spot(Dog{"Spot"}); + + std::cout << name(spot) << "\n"; // Spot the dog + // end::dispatch[] + + BOOST_TEST(cout.str() == "Spot the dog\n"); + } +} diff --git a/doc/modules/ROOT/snippets/virtual_any.cpp b/doc/modules/ROOT/snippets/virtual_any.cpp new file mode 100644 index 00000000..3abd68f5 --- /dev/null +++ b/doc/modules/ROOT/snippets/virtual_any.cpp @@ -0,0 +1,163 @@ +// Copyright (c) 2018-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 +#include +#include +#include + +#define BOOST_TEST_MODULE openmethod +#include + +#include "capture.hpp" + +using namespace boost::openmethod; + +namespace std_any { + +// tag::classes[] +struct Dog { + std::string name; +}; + +// `std::any` is the common base of the types it may contain. The types are +// registered automatically: naming one in an overrider - or storing a value +// in a `virtual_std_any` - registers it. +// end::classes[] + +// tag::method[] +BOOST_OPENMETHOD(name, (const virtual_std_any&), std::string); + +// An overrider takes the contained value... +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { + return name; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const int& value), std::string) { + return std::to_string(value) + " the integer"; +} + +// ...or the `virtual_any` itself, which makes it a catch-all. +BOOST_OPENMETHOD_OVERRIDE(name, (const virtual_std_any& value), std::string) { + return value.get().has_value() ? "something else" : "nothing"; +} +// end::method[] + +} // namespace std_any + +namespace boost_any { + +// tag::boost_classes[] +struct Dog { + std::string name; +}; + +// `boost::any` is a root class of its own, distinct from the one used for +// `std::any`, so both may be used in the same program and registry. +BOOST_OPENMETHOD(name, (const virtual_boost_any&), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { + return name; +} +// end::boost_classes[] + +} // namespace boost_any + +BOOST_AUTO_TEST_CASE(std_any_examples) { + using namespace std_any; + + initialize(); + + { + capture_cout cout; + + // tag::dispatch[] + virtual_std_any spot = Dog{"Spot"}; + + std::cout << name(spot) << "\n"; // Spot the dog + + // The value converts to a temporary `virtual_std_any` at the call + // site, which registers `float`. It has no overrider of its own, + // so the catch-all applies. + std::cout << name(3.14f) << "\n"; // something else + // end::dispatch[] + + BOOST_TEST(cout.str() == "Spot the dog\nsomething else\n"); + } + + { + capture_cout cout; + + // tag::from_any[] + std::any spot_any = Dog{"Spot"}; + + // the v-table pointer is looked up from the type of the value the + // `any` contains + virtual_std_any spot = spot_any; + + std::cout << name(spot) << "\n"; // Spot the dog + // end::from_any[] + + BOOST_TEST(cout.str() == "Spot the dog\n"); + } + + { + capture_cout cout; + + // tag::from_value[] + // the type is known at compile time, so the v-table pointer is read + // from a static variable - there is no lookup + virtual_std_any answer = 42; + + std::cout << name(answer) << "\n"; // 42 the integer + // end::from_value[] + + BOOST_TEST(cout.str() == "42 the integer\n"); + } + + { + capture_cout cout; + + // tag::emplace[] + virtual_std_any value; + + value.emplace("Felix the cat"); + + std::cout << name(value) << "\n"; // Felix the cat + // end::emplace[] + + BOOST_TEST(cout.str() == "Felix the cat\n"); + } +} + +BOOST_AUTO_TEST_CASE(boost_any_examples) { + using namespace boost_any; + + initialize(); + + { + capture_cout cout; + + // tag::boost_dispatch[] + virtual_boost_any felix = std::string("Felix the cat"); + + std::cout << name(felix) << "\n"; // Felix the cat + // end::boost_dispatch[] + + BOOST_TEST(cout.str() == "Felix the cat\n"); + } +} diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index 3adf2e51..3143432a 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -459,13 +459,10 @@ class use_classes { //! given argument type and registry, that overload is used to acquire a v-table //! pointer instead of the registry's @ref policies::vptr policy. //! -//! The library uses `boost_openmethod_vptr` (if found): -//! -//! @li when dispatching via a @ref virtual_ parameter (*not* a @ref -//! virtual_ptr); -//! -//! @li when a @ref virtual_ptr is constructed from, or assigned, a reference or -//! a pointer to an object (*not* by the "final" constructs). +//! The library uses `boost_openmethod_vptr`, if found, when dispatching via a +//! @ref virtual_ parameter. It is not used by @ref virtual_ptr; wrapping an +//! object that has an overload in a @ref virtual_ptr is rejected at compile +//! time. //! //! @par Requirements //! @@ -597,12 +594,25 @@ constexpr bool has_vptr_fn = std::is_same_v< std::declval(), std::declval())), vptr_type>; +BOOST_OPENMETHOD_DETAIL_HAS_STATIC_FN(vptr); + template decltype(auto) acquire_vptr(const ArgType& arg) { + // A class with a boost_openmethod_vptr overload does not need to be + // wrapped: virtual_ptr and the hook fill the same goal, fast access + // to the v-table pointer. The hook also returns the vptr by value, + // which indirect registries cannot store (see box_vptr). + static_assert( + !has_vptr_fn, + "do not wrap an object that has a boost_openmethod_vptr overload " + "in a virtual_ptr; call methods directly on the object"); + Registry::require_initialized(); - if constexpr (detail::has_vptr_fn) { - return boost_openmethod_vptr(arg, static_cast(nullptr)); + if constexpr (has_vptr< + virtual_traits, + const ArgType&>) { + return virtual_traits::vptr(arg); } else { return Registry::template policy::dynamic_vptr(arg); } @@ -657,8 +667,7 @@ inline vptr_type null_vptr = nullptr; //! //! @par Example //! -//! See [the default-registry overload](xref:reference:boost/openmethod/final_virtual_ptr-08.adoc#_example) -//! for an example. +//! include:virtual_ptr.cpp#non_polymorphic_classes;final_virtual_ptr //! //! @tparam Registry A @ref registry. //! @tparam Arg The type of the argument. @@ -819,10 +828,12 @@ class virtual_ptr { //! Construct a `virtual_ptr` from a reference to an object //! - //! The pointer to the v-table is obtained by calling - //! @ref boost_openmethod_vptr if a suitable overload exists, or the - //! @ref policies::VptrFn::dynamic_vptr of the registry's - //! `vptr` policy otherwise. + //! The pointer to the v-table is obtained from @ref virtual_traits, + //! if it provides a `vptr` function, or from the + //! @ref policies::VptrFn::dynamic_vptr of the registry's `vptr` + //! policy otherwise. An object with a @ref boost_openmethod_vptr + //! overload is rejected at compile time: it carries its own v-table + //! pointer, and does not need to be wrapped in a `virtual_ptr`. //! //! @param other A reference to a polymorphic object //! @@ -854,10 +865,12 @@ class virtual_ptr { //! Construct a `virtual_ptr` from a pointer to an object //! - //! The pointer to the v-table is obtained by calling - //! @ref boost_openmethod_vptr if a suitable overload exists, or the - //! @ref policies::VptrFn::dynamic_vptr of the registry's - //! `vptr` policy otherwise. + //! The pointer to the v-table is obtained from @ref virtual_traits, + //! if it provides a `vptr` function, or from the + //! @ref policies::VptrFn::dynamic_vptr of the registry's `vptr` + //! policy otherwise. An object with a @ref boost_openmethod_vptr + //! overload is rejected at compile time: it carries its own v-table + //! pointer, and does not need to be wrapped in a `virtual_ptr`. //! //! @par Example //! include:virtual_ptr.cpp#ctor_pointer @@ -923,10 +936,12 @@ class virtual_ptr { //! Assign a `virtual_ptr` from a reference to an object //! - //! The pointer to the v-table is obtained by calling - //! @ref boost_openmethod_vptr if a suitable overload exists, or the - //! @ref policies::VptrFn::dynamic_vptr of the registry's - //! `vptr` policy otherwise. + //! The pointer to the v-table is obtained from @ref virtual_traits, + //! if it provides a `vptr` function, or from the + //! @ref policies::VptrFn::dynamic_vptr of the registry's `vptr` + //! policy otherwise. An object with a @ref boost_openmethod_vptr + //! overload is rejected at compile time: it carries its own v-table + //! pointer, and does not need to be wrapped in a `virtual_ptr`. //! //! @par Example //! include:virtual_ptr.cpp#assign_ref @@ -961,10 +976,12 @@ class virtual_ptr { //! Assign a `virtual_ptr` from a pointer to an object //! - //! The pointer to the v-table is obtained by calling - //! @ref boost_openmethod_vptr if a suitable overload exists, or the - //! @ref policies::VptrFn::dynamic_vptr of the registry's - //! `vptr` policy otherwise. + //! The pointer to the v-table is obtained from @ref virtual_traits, + //! if it provides a `vptr` function, or from the + //! @ref policies::VptrFn::dynamic_vptr of the registry's `vptr` + //! policy otherwise. An object with a @ref boost_openmethod_vptr + //! overload is rejected at compile time: it carries its own v-table + //! pointer, and does not need to be wrapped in a `virtual_ptr`. //! //! @par Example //! include:virtual_ptr.cpp#assign_pointer @@ -1963,7 +1980,9 @@ struct validate_method_parameter< //! 2. If @ref boost_openmethod_vptr can be called with `result` and a //! `Registry*`, and it returns a `vptr_type`, call it. //! -//! 3. Call the @ref policies::VptrFn::dynamic_vptr of the registry's `vptr` +//! 3. If @ref virtual_traits provides a `vptr` function, call it. +//! +//! 4. Call the @ref policies::VptrFn::dynamic_vptr of the registry's `vptr` //! policy. //! //! @par N2216 Handling of Ambiguous Calls @@ -2179,7 +2198,7 @@ class method void resolve_type_ids(); - template + template auto vptr(const ArgType& arg) const -> vptr_type; template @@ -2333,7 +2352,7 @@ method::operator()( typename BOOST_OPENMETHOD_DETAIL_UNLESS_MRDOCS StripVirtualDecorator::type... args) const -> ReturnType { using namespace detail; - auto pf = resolve(parameter_traits::peek(args)...); + auto pf = resolve(args...); return pf(std::forward::type>( args)...); @@ -2365,13 +2384,23 @@ BOOST_FORCEINLINE template< typename Id, typename... Parameters, typename ReturnType, class Registry> -template +template BOOST_FORCEINLINE auto method::vptr( const ArgType& arg) const -> vptr_type { if constexpr (detail::is_virtual_ptr) { return arg.vptr(); } else { - return detail::acquire_vptr(arg); + decltype(auto) obj = virtual_traits::peek(arg); + + if constexpr (detail::has_vptr_fn) { + return boost_openmethod_vptr(obj, static_cast(nullptr)); + } else if constexpr (detail::has_vptr< + virtual_traits, + decltype(obj)>) { + return virtual_traits::vptr(obj); + } else { + return Registry::template policy::dynamic_vptr(obj); + } } } @@ -2388,7 +2417,7 @@ method::resolve_uni( using namespace boost::mp11; if constexpr (is_virtual>::value) { - vptr_type vtbl = vptr(arg); + vptr_type vtbl = vptr>>(arg); return vtbl[this->slots_strides[0]]; } else { return resolve_uni>(more_args...); @@ -2407,7 +2436,7 @@ method::resolve_multi_first( using namespace boost::mp11; if constexpr (is_virtual>::value) { - vptr_type vtbl = vptr(arg); + vptr_type vtbl = vptr>>(arg); std::size_t slot = this->slots_strides[0]; // The first virtual parameter is special. Since its stride is @@ -2437,7 +2466,7 @@ method::resolve_multi_next( using namespace boost::mp11; if constexpr (is_virtual>::value) { - vptr_type vtbl = vptr(arg); + vptr_type vtbl = vptr>>(arg); std::size_t slot = this->slots_strides[VirtualArg]; std::size_t stride = this->slots_strides[Arity + VirtualArg - 1]; dispatch = dispatch + vtbl[slot].i * stride; @@ -2745,6 +2774,37 @@ struct VirtualTraits { //! @return A reference to an object. static auto peek(T arg) -> const virtual_type&; + // Added by the `std::any` interop, under the name `type_vptr`. An `any` + // dispatches on the type of the value it contains, which the rtti policy + // cannot see: `dynamic_type` on the `any` itself yields the wrapper. + + //! Returns a *reference* to the v-table pointer for an object. + //! + //! `vptr` is optional. It is called on the object returned by @ref peek, + //! not on the method argument itself. A method acquires the v-table + //! pointer of a virtual argument from the first of the following that is + //! available: a `boost_openmethod_vptr` function, found by ADL on the + //! peeked object; `vptr`; @ref policies::VptrFn::dynamic_vptr of the + //! registry's @ref policies::vptr policy. + //! + //! Implement `vptr` only if the v-table pointer cannot be obtained from + //! the dynamic type of the peeked object, as reported by the registry's + //! @ref policies::rtti policy, or if it is already at hand. The former is + //! the case for `any`-like types: their dynamic type is the wrapper, not + //! the value they contain. The `std::any` specializations read the + //! @ref type_id of the contained value from `arg.type()`, and pass it to + //! @ref policies::VptrFn::vptr. The latter is the case for a wide type + //! that caches the v-table pointer: @ref virtual_any returns the one it + //! acquired when it was created, without a lookup. + //! + //! `vptr` must return a *reference*, not a value, so that the caller + //! observes the current v-table pointer if the registry contains the + //! @ref policies::indirect_vptr policy and `initialize` is called again. + //! + //! @param arg The object returned by @ref peek. + //! @return A reference to the v-table pointer for `arg`. + static auto vptr(const virtual_type& arg) -> const vptr_type&; + //! Casts a virtual argument. //! //! `cast` is responsible for passing virtual arguments from method to diff --git a/include/boost/openmethod/inplace_vptr.hpp b/include/boost/openmethod/inplace_vptr.hpp index 7505bf32..cf1328b0 100644 --- a/include/boost/openmethod/inplace_vptr.hpp +++ b/include/boost/openmethod/inplace_vptr.hpp @@ -76,6 +76,10 @@ class inplace_vptr_base_tag {}; //! @ref policies::vptr policy, nor any policy it depends on (like @ref //! policies::type_hash). //! +//! An object that embeds its v-table pointer does not need to be wrapped +//! in a @ref virtual_ptr - the two fill the same goal, fast access to the +//! v-table pointer - and wrapping one is rejected at compile time. +//! //! If `Registry` contains the @ref has_indirect_vptr policy, the v-table //! pointer is stored as a pointer to a pointer, and remains valid after a call //! to @ref initialize. diff --git a/include/boost/openmethod/interop/boost_any.hpp b/include/boost/openmethod/interop/boost_any.hpp new file mode 100644 index 00000000..fd3d5ffd --- /dev/null +++ b/include/boost/openmethod/interop/boost_any.hpp @@ -0,0 +1,343 @@ +// Copyright (c) 2018-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_BOOST_ANY_HPP +#define BOOST_OPENMETHOD_INTEROP_BOOST_ANY_HPP + +#include +#include +#include +#include + +// Dispatch on the type contained in a `boost::any`. +// +// This interop is based on a design contributed by Steven Watanabe: +// https://github.com/boostorg/openmethod/issues/21 + +namespace boost::openmethod { + +namespace detail { + +template +struct validate_method_parameter, Registry, void> + : std::true_type {}; + +template +struct validate_method_parameter, Registry, void> + : std::true_type {}; + +template +struct validate_method_parameter, 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 +// other policy the lookup key is meaningless, and `type_id` being +// `const void*`, nothing would diagnose it. +template +constexpr void assert_std_rtti_boost_any() { + static_assert( + std::is_base_of_v< + policies::std_rtti, + find_first_derived_of< + policies::rtti, typename Registry::policy_list>>, + "requires standard RTTI"); +} + +} // namespace detail + +//! Specialize virtual_traits for `const boost::any&` (const reference). +//! +//! Dispatch is based on the runtime type of the value stored in the `any`, +//! obtained via `boost::any::type()`. +//! +//! @tparam Registry A @ref registry. +template +struct virtual_traits { + //! The type used for dispatch. + using virtual_type = boost::any; + + //! Returns a const reference to the `any` argument. + //! @param arg A reference to a `boost::any`. + //! @return A const reference to `arg`. + static auto peek(const boost::any& arg) -> const boost::any& { + return arg; + } + + //! Returns a *reference* to a v-table pointer for an object. + //! + //! Acquires the dynamic @ref type_id of the value stored in `arg`, using + //! `boost::any::type()`. This requires the registry's @ref rtti policy to + //! be @ref std_rtti; the requirement is enforced with a `static_assert`. + //! `boost::any::type()` yields the same `std::type_info` object as + //! `&typeid(T)`, provided Boost.TypeIndex uses `stl_type_index`. + //! Registers `boost::any` - the root class of the contained types - in + //! `Registry`. + //! + //! Passes the type id to the registry's @ref policies::vptr policy, which + //! must provide @ref policies::VptrFn::vptr. Both + //! @ref policies::vptr_vector and @ref policies::vptr_map do. + //! + //! If the registry has a @ref type_hash policy, uses it to convert the + //! type id to an index; otherwise, uses the type_id as the index. + //! + //! If the registry contains the @ref runtime_checks policy, verifies + //! that the index falls within the limits of the vector. If it does + //! not, and if the registry contains a @ref error_handler policy, calls + //! its @ref error function with a @ref missing_class value, then + //! terminates the program with @ref abort. + //! + //! @param arg A reference to a const `any`. + //! @return A reference to the v-table pointer for the stored value. + static auto vptr(const boost::any& arg) -> const vptr_type& { + detail::assert_std_rtti_boost_any(); + (void)&detail::use_any_classes; + return Registry::vptr::vptr(&arg.type()); + } + + //! Cast to a type. + //! + //! If `U` is the `any` itself (by any reference category), returns + //! `arg` unchanged, which is how a catch-all overrider is written. + //! Otherwise, extracts the stored value using `boost::any_cast`, and + //! registers `U`, stripped of reference and cv-qualifiers, in + //! `Registry` as a class derived from `boost::any`. + //! + //! Since the `any` argument is const, `U` cannot be a mutable reference. + //! `boost::any_cast` rewrites `U` to a const reference for a const `any`, + //! and would fail inside Boost.Any; this overload is removed from the + //! overload set instead. + //! + //! @tparam U The target type (e.g. `const Dog&`, `Dog`). + //! @param arg A reference to a const `boost::any` method argument. + //! @return The value stored in `arg`, cast to `U`. + template< + typename U, + typename = std::enable_if_t< + !std::is_reference_v || + std::is_const_v>>> + static auto cast(const boost::any& arg) -> decltype(auto) { + if constexpr (std::is_same_v< + std::remove_cv_t>, + boost::any>) { + return (arg); + } else { + (void)&detail::use_any_classes< + Registry, boost::any, std::decay_t>; + return boost::any_cast(arg); + } + } +}; + +//! Specialize virtual_traits for `boost::any&` (mutable reference). +//! +//! Dispatch is based on the runtime type of the value stored in the `any`, +//! obtained via `boost::any::type()`. +//! +//! @tparam Registry A @ref registry. +template +struct virtual_traits { + //! The type used for dispatch. + using virtual_type = boost::any; + + //! Returns a const reference to the `any` argument. + //! @param arg A reference to a `boost::any`. + //! @return A const reference to `arg`. + static auto peek(const boost::any& arg) -> const boost::any& { + return arg; + } + + //! Returns a *reference* to a v-table pointer for an object. + //! + //! Acquires the dynamic @ref type_id of the value stored in `arg`, using + //! `boost::any::type()`. This requires the registry's @ref rtti policy to + //! be @ref std_rtti; the requirement is enforced with a `static_assert`. + //! `boost::any::type()` yields the same `std::type_info` object as + //! `&typeid(T)`, provided Boost.TypeIndex uses `stl_type_index`. + //! Registers `boost::any` - the root class of the contained types - in + //! `Registry`. + //! + //! Passes the type id to the registry's @ref policies::vptr policy, which + //! must provide @ref policies::VptrFn::vptr. Both + //! @ref policies::vptr_vector and @ref policies::vptr_map do. + //! + //! If the registry has a @ref type_hash policy, uses it to convert the + //! type id to an index; otherwise, uses the type_id as the index. + //! + //! If the registry contains the @ref runtime_checks policy, verifies + //! that the index falls within the limits of the vector. If it does + //! not, and if the registry contains a @ref error_handler policy, calls + //! its @ref error function with a @ref missing_class value, then + //! terminates the program with @ref abort. + //! + //! @param arg A reference to a `boost::any`. + //! @return A reference to the v-table pointer for the stored value. + static auto vptr(const boost::any& arg) -> const vptr_type& { + detail::assert_std_rtti_boost_any(); + (void)&detail::use_any_classes; + return Registry::vptr::vptr(&arg.type()); + } + + //! Cast to a type. + //! + //! If `U` is the `any` itself (by any reference category), returns + //! `arg` unchanged, which is how a catch-all overrider is written. + //! Otherwise, extracts the stored value using `boost::any_cast`, and + //! registers `U`, stripped of reference and cv-qualifiers, in + //! `Registry` as a class derived from `boost::any`. Supports mutable + //! references (e.g. `Dog&`) because the `any` argument is not const; + //! modifications through the result are visible through the `any`. + //! + //! `U` cannot be an rvalue reference. Unlike `std::any_cast`, + //! `boost::any_cast` binds an rvalue reference to the value stored in an + //! lvalue `any`; moving the value out must go through an explicit + //! `virtual_` parameter, so this overload is removed from + //! the overload set. + //! + //! @tparam U The target type (e.g. `Dog&`, `const Dog&`, `Dog`). + //! @param arg A mutable reference to the `boost::any` method argument. + //! @return The value stored in `arg`, cast to `U`. + template< + typename U, typename = std::enable_if_t>> + static auto cast(boost::any& arg) -> decltype(auto) { + if constexpr (std::is_same_v< + std::remove_cv_t>, + boost::any>) { + return (arg); + } else { + (void)&detail::use_any_classes< + Registry, boost::any, std::decay_t>; + return boost::any_cast(arg); + } + } +}; + +//! Specialize virtual_traits for `boost::any&&` (xvalue reference). +//! +//! Dispatch is based on the runtime type of the value stored in the `any`, +//! obtained via `boost::any::type()`. +//! +//! @tparam Registry A @ref registry. +template +struct virtual_traits { + //! The type used for dispatch. + using virtual_type = boost::any; + + //! Returns a const reference to the `any` argument. + //! @param arg A reference to a `boost::any`. + //! @return A const reference to `arg`. + static auto peek(const boost::any& arg) -> const boost::any& { + return arg; + } + + //! Returns a *reference* to a v-table pointer for an object. + //! + //! Acquires the dynamic @ref type_id of the value stored in `arg`, using + //! `boost::any::type()`. This requires the registry's @ref rtti policy to + //! be @ref std_rtti; the requirement is enforced with a `static_assert`. + //! `boost::any::type()` yields the same `std::type_info` object as + //! `&typeid(T)`, provided Boost.TypeIndex uses `stl_type_index`. + //! Registers `boost::any` - the root class of the contained types - in + //! `Registry`. + //! + //! Passes the type id to the registry's @ref policies::vptr policy, which + //! must provide @ref policies::VptrFn::vptr. Both + //! @ref policies::vptr_vector and @ref policies::vptr_map do. + //! + //! If the registry has a @ref type_hash policy, uses it to convert the + //! type id to an index; otherwise, uses the type_id as the index. + //! + //! If the registry contains the @ref runtime_checks policy, verifies + //! that the index falls within the limits of the vector. If it does + //! not, and if the registry contains a @ref error_handler policy, calls + //! its @ref error function with a @ref missing_class value, then + //! terminates the program with @ref abort. + //! + //! @param arg A reference to a `boost::any`. + //! @return A reference to the v-table pointer for the stored value. + static auto vptr(const boost::any& arg) -> const vptr_type& { + detail::assert_std_rtti_boost_any(); + (void)&detail::use_any_classes; + return Registry::vptr::vptr(&arg.type()); + } + + //! Cast to a type. + //! + //! If `U` is the `any` itself (by any reference category), returns + //! `arg` unchanged, which is how a catch-all overrider is written. + //! Otherwise, extracts the stored value using `boost::any_cast`, and + //! registers `U`, stripped of reference and cv-qualifiers, in + //! `Registry` as a class derived from `boost::any`. + //! + //! `U` cannot be a mutable lvalue reference: that would bind a reference + //! to the value contained in a temporary. Boost.Any rejects it with a + //! static assertion; this overload is removed from the overload set + //! instead, for consistency with the other reference categories. + //! + //! @tparam U The target type (e.g. `Dog&&`, `const Dog&`, `Dog`). + //! @param arg An rvalue reference to the `boost::any` method argument. + //! @return The value stored in `arg`, cast to `U`. + template< + typename U, + typename = std::enable_if_t< + !std::is_lvalue_reference_v || + std::is_const_v>>> + static auto cast(boost::any&& arg) -> decltype(auto) { + if constexpr (std::is_same_v< + std::remove_cv_t>, + boost::any>) { + return std::move(arg); + } else { + (void)&detail::use_any_classes< + Registry, boost::any, std::decay_t>; + return boost::any_cast(std::move(arg)); + } + } +}; + +//! Alias for a `virtual_any`, in the default registry. +//! +//! With another registry, use `virtual_any` directly. +//! +//! The root class of the contained types is `boost::any`, distinct from the +//! `std::any` root, so both may be used in the same program, and with the +//! same registry. +//! +//! @par Example +//! include:virtual_any.cpp#boost_classes;boost_dispatch +//! +//! @see [Interoperation with `any`](xref:ROOT:interop_any.adoc) +using virtual_boost_any = virtual_any; + +// The primary final_virtual_ptr would silently use static_vptr +// - the v-table of the `any` root class, not of the contained value. +// Delete the combination. Both call forms need covering: the non-template +// overloads catch calls that deduce the default registry, and are removed +// from consideration when an explicit template argument list is given, so +// the Registry-only templates - more specialized than the primary - catch +// those. +// +// Hidden from the reference: they are a guard, not API, and six deleted +// overloads would crowd the `final_virtual_ptr` overload list. + +#ifndef __MRDOCS__ +template +void final_virtual_ptr(const boost::any&) = delete; +template +void final_virtual_ptr(boost::any&) = delete; +template +void final_virtual_ptr(boost::any&&) = delete; +void final_virtual_ptr(const boost::any&) = delete; +void final_virtual_ptr(boost::any&) = delete; +void final_virtual_ptr(boost::any&&) = delete; +#endif + +namespace aliases { +using boost::openmethod::virtual_boost_any; +} // namespace aliases + +} // namespace boost::openmethod + +#endif diff --git a/include/boost/openmethod/interop/boost_type_erasure.hpp b/include/boost/openmethod/interop/boost_type_erasure.hpp new file mode 100644 index 00000000..a3cedc98 --- /dev/null +++ b/include/boost/openmethod/interop/boost_type_erasure.hpp @@ -0,0 +1,659 @@ +// Copyright (c) 2018-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_BOOST_TYPE_ERASURE_HPP +#define BOOST_OPENMETHOD_INTEROP_BOOST_TYPE_ERASURE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +// Dispatch on the type contained in a boost::type_erasure::any. +// +// The Concept must contain boost::type_erasure::typeid_<> - which +// `relaxed` already implies - so that `typeid_of` can identify the +// contained value. Dispatch is on the `std::type_info` object returned by +// `typeid_of`: the type of the contained value for the owning any +// (`any`), or the type *bound at construction* for the any +// references (`any`, `any`) - never +// the C++ RTTI dynamic type of the referent. +// +// Supported virtual parameter forms: +// - `virtual_&>`, `virtual_&>`, +// `virtual_&&>` - the owning any, by reference, like +// `std::any`; +// - `virtual_>` and +// `virtual_>` - the any references, by +// value (they are cheap, two-word handles); +// - `virtual_any>` - looks the v-table pointer up once, at +// construction. The Concept needs `relaxed` for virtual_any's default +// constructor and assignment, and `copy_constructible<>` for copies. +// +// The rvalue-reference placeholder (`_self&&`), and placeholders other +// than `_self`, are not supported. +// +// The bound types are registered automatically, as classes derived from +// the owning any - the root class for the Concept: naming a type as an +// overrider parameter registers it; so does storing a value in a +// virtual_any. +// +// In addition, `openmethod_vptr` is a Boost.TypeErasure concept that +// stores the v-table pointer for the bound type in the any's own dispatch +// table, making every any on that Concept intrinsically polymorphic: calls +// resolve in constant time, without hashing the result of `typeid_of`, +// and binding a value to the any registers its type. +// +// This interop is based on a design contributed by Steven Watanabe: +// https://github.com/boostorg/openmethod/issues/21 + +namespace boost::openmethod { + +namespace detail { + +// Classification of the placeholder of an any. `T = _self` +// (or any non-reference placeholder): the any owns the value. `T = +// _self&`: non-owning handle to a mutable referent. Anything else +// (`const _self&`, `_self&&`) is treated as binding a value that may not +// be mutated or moved from. +template +constexpr bool te_owning = !std::is_reference_v; + +template +constexpr bool te_mutable_bound = std::is_lvalue_reference_v && + !std::is_const_v>; + +// Does U, an overrider parameter type, require mutable access? +template +constexpr bool te_mutable_target = std::is_lvalue_reference_v && + !std::is_const_v>; + +// Is U, an overrider parameter type, the `any` itself (by value or by +// any reference category)? Then the argument is passed through +// unchanged - the catch-all overrider case - instead of going through +// any_cast, which would throw unless the any contains an any. +template +constexpr bool te_pass_through = + std::is_same_v>, Any>; + +// The canonical root class for a Concept is the owning any. All the +// virtual_traits below use it as their virtual_type, whatever the +// placeholder of the parameter, so methods and overriders agree on a +// single registered root per Concept. + +template +struct validate_method_parameter< + virtual_&>, Registry, void> + : std::true_type {}; + +template +struct validate_method_parameter< + virtual_&>, Registry, void> + : std::true_type {}; + +template +struct validate_method_parameter< + virtual_&&>, Registry, void> + : std::true_type {}; + +template +struct validate_method_parameter< + virtual_>, Registry, void> + : std::true_type {}; + +template +struct validate_method_parameter< + virtual_>, Registry, void> + : std::true_type {}; + +template +struct validate_method_parameter< + virtual_>, Registry, + void> : std::false_type { + static_assert( + false_t, "an owning type_erasure::any must be passed by reference"); +}; + +// `boost::type_erasure::typeid_of` yields a `std::type_info`, which is a valid +// `type_id` only for an rtti policy that identifies classes by `&typeid(T)`. +// Under any other policy the lookup key is meaningless, and `type_id` being +// `const void*`, nothing would diagnose it. The `openmethod_vptr` concept does +// not go through `typeid_of`, and is deliberately not covered. +template +constexpr void assert_std_rtti_type_erasure() { + static_assert( + std::is_base_of_v< + policies::std_rtti, + find_first_derived_of< + policies::rtti, typename Registry::policy_list>>, + "requires standard RTTI"); +} + +} // namespace detail + +//! Specialize virtual_traits for `const boost::type_erasure::any&`. +//! +//! Dispatch is based on the type of the value bound to the `any`, +//! obtained via `boost::type_erasure::typeid_of`. `Concept` must contain +//! `boost::type_erasure::typeid_<>`; `relaxed` implies it. +//! +//! This specialization serves the owning `any` (`any`) and, +//! through a const `any`, the any references. +//! +//! @tparam C The `any`'s Concept. +//! @tparam T The `any`'s placeholder. +//! @tparam Registry A @ref registry. +//! +//! @see [Interoperation with Boost.TypeErasure](xref:ROOT:interop_type_erasure.adoc) +template +struct virtual_traits&, Registry> { + //! The type used for dispatch: the owning `any` for `C`. + using virtual_type = boost::type_erasure::any; + + //! Returns a const reference to the `any` argument. + //! @param arg A reference to an `any`. + //! @return A const reference to `arg`. + static auto peek(const boost::type_erasure::any& arg) + -> const boost::type_erasure::any& { + return arg; + } + + //! Returns a *reference* to a v-table pointer for the bound value. + //! + //! Looks up the @ref type_id returned by + //! `boost::type_erasure::typeid_of` in the registry's `vptr` policy. + //! + //! This requires the registry's @ref rtti policy to be @ref std_rtti; + //! the requirement is enforced with a `static_assert`. Registers the + //! owning `any` - the root class of the bound types - in `Registry`. + //! + //! @param arg A reference to a const `any`. + //! @return A reference to the v-table pointer for the bound value. + static auto + vptr(const boost::type_erasure::any& arg) -> const vptr_type& { + detail::assert_std_rtti_type_erasure(); + (void)&detail::use_any_classes>; + return Registry::vptr::vptr(&boost::type_erasure::typeid_of(arg)); + } + + //! Cast to a type. + //! + //! Extracts the bound value using `boost::type_erasure::any_cast`, + //! and registers `U`, stripped of reference and cv-qualifiers, in + //! `Registry` as a class derived from the owning `any`. + //! Since the `any` is const, `U` can be a mutable reference only for + //! the mutable any reference (`any`), whose + //! referent stays mutable through a const `any`. Rvalue references + //! are never allowed; the overloads are removed from the overload + //! set. + //! + //! @tparam U The target type (e.g. `const Dog&`, `Dog`). + //! @param arg A reference to a const `any` method argument. + //! @return The value bound to `arg`, cast to `U`. + template< + typename U, + typename = std::enable_if_t< + !std::is_rvalue_reference_v && + (!detail::te_mutable_target || detail::te_mutable_bound)>> + static auto + cast(const boost::type_erasure::any& arg) -> decltype(auto) { + if constexpr (detail::te_pass_through< + U, boost::type_erasure::any>) { + return (arg); + } else { + (void)&detail::use_any_classes< + Registry, boost::type_erasure::any, std::decay_t>; + return boost::type_erasure::any_cast(arg); + } + } +}; + +//! Specialize virtual_traits for `boost::type_erasure::any&` (mutable +//! reference). +//! +//! Dispatch is based on the type of the value bound to the `any`, +//! obtained via `boost::type_erasure::typeid_of`. `Concept` must contain +//! `boost::type_erasure::typeid_<>`; `relaxed` implies it. +//! +//! @tparam C The `any`'s Concept. +//! @tparam T The `any`'s placeholder. +//! @tparam Registry A @ref registry. +//! +//! @see [Interoperation with Boost.TypeErasure](xref:ROOT:interop_type_erasure.adoc) +template +struct virtual_traits&, Registry> { + //! The type used for dispatch: the owning `any` for `C`. + using virtual_type = boost::type_erasure::any; + + //! Returns a const reference to the `any` argument. + //! @param arg A reference to an `any`. + //! @return A const reference to `arg`. + static auto peek(const boost::type_erasure::any& arg) + -> const boost::type_erasure::any& { + return arg; + } + + //! Returns a *reference* to a v-table pointer for the bound value. + //! + //! Looks up the @ref type_id returned by + //! `boost::type_erasure::typeid_of` in the registry's `vptr` policy. + //! + //! This requires the registry's @ref rtti policy to be @ref std_rtti; + //! the requirement is enforced with a `static_assert`. Registers the + //! owning `any` - the root class of the bound types - in `Registry`. + //! + //! @param arg A reference to an `any`. + //! @return A reference to the v-table pointer for the bound value. + static auto + vptr(const boost::type_erasure::any& arg) -> const vptr_type& { + detail::assert_std_rtti_type_erasure(); + (void)&detail::use_any_classes>; + return Registry::vptr::vptr(&boost::type_erasure::typeid_of(arg)); + } + + //! Cast to a type. + //! + //! Extracts the bound value using `boost::type_erasure::any_cast`, + //! and registers `U`, stripped of reference and cv-qualifiers, in + //! `Registry` as a class derived from the owning `any`. + //! Supports mutable references (e.g. `Dog&`), except through the + //! const any reference (`any`). `U` cannot + //! be an rvalue reference: moving the value out must go through an + //! explicit rvalue-reference parameter. The disallowed overloads are + //! removed from the overload set. + //! + //! @tparam U The target type (e.g. `Dog&`, `const Dog&`, `Dog`). + //! @param arg A mutable reference to the `any` method argument. + //! @return The value bound to `arg`, cast to `U`. + template< + typename U, + typename = std::enable_if_t< + !std::is_rvalue_reference_v && + (!detail::te_mutable_target || detail::te_owning || + detail::te_mutable_bound)>> + static auto cast(boost::type_erasure::any& arg) -> decltype(auto) { + if constexpr (detail::te_pass_through< + U, boost::type_erasure::any>) { + return (arg); + } else { + (void)&detail::use_any_classes< + Registry, boost::type_erasure::any, std::decay_t>; + return boost::type_erasure::any_cast(arg); + } + } +}; + +//! Specialize virtual_traits for `boost::type_erasure::any&&` (xvalue +//! reference). +//! +//! Dispatch is based on the type of the value bound to the `any`, +//! obtained via `boost::type_erasure::typeid_of`. `Concept` must contain +//! `boost::type_erasure::typeid_<>`; `relaxed` implies it. +//! +//! @tparam C The `any`'s Concept. +//! @tparam T The `any`'s placeholder. +//! @tparam Registry A @ref registry. +//! +//! @see [Interoperation with Boost.TypeErasure](xref:ROOT:interop_type_erasure.adoc) +template +struct virtual_traits&&, Registry> { + //! The type used for dispatch: the owning `any` for `C`. + using virtual_type = boost::type_erasure::any; + + //! Returns a const reference to the `any` argument. + //! @param arg A reference to an `any`. + //! @return A const reference to `arg`. + static auto peek(const boost::type_erasure::any& arg) + -> const boost::type_erasure::any& { + return arg; + } + + //! Returns a *reference* to a v-table pointer for the bound value. + //! + //! Looks up the @ref type_id returned by + //! `boost::type_erasure::typeid_of` in the registry's `vptr` policy. + //! + //! This requires the registry's @ref rtti policy to be @ref std_rtti; + //! the requirement is enforced with a `static_assert`. Registers the + //! owning `any` - the root class of the bound types - in `Registry`. + //! + //! @param arg A reference to a const `any`. + //! @return A reference to the v-table pointer for the bound value. + static auto + vptr(const boost::type_erasure::any& arg) -> const vptr_type& { + detail::assert_std_rtti_type_erasure(); + (void)&detail::use_any_classes>; + return Registry::vptr::vptr(&boost::type_erasure::typeid_of(arg)); + } + + //! Cast to a type. + //! + //! Extracts the bound value using `boost::type_erasure::any_cast`, + //! and registers `U`, stripped of reference and cv-qualifiers, in + //! `Registry` as a class derived from the owning `any`. + //! `boost::type_erasure::any_cast` has no rvalue overload, so, for an + //! rvalue-reference `U`, the result of a mutable-reference cast is + //! moved - only for the owning `any`, since the rvalue-ness of an + //! any reference says nothing about the referent. Casting to a + //! value also moves for the owning `any`, and copies otherwise. The + //! disallowed overloads are removed from the overload set. + //! + //! @tparam U The target type (e.g. `Dog&&`, `const Dog&`, `Dog`). + //! @param arg An rvalue reference to the `any` method argument. + //! @return The value bound to `arg`, cast to `U`. + template< + typename U, + typename = std::enable_if_t< + (!std::is_rvalue_reference_v || detail::te_owning) && + (!detail::te_mutable_target || detail::te_owning || + detail::te_mutable_bound)>> + static auto cast(boost::type_erasure::any&& arg) -> decltype(auto) { + if constexpr (detail::te_pass_through< + U, boost::type_erasure::any>) { + return std::move(arg); + } else { + (void)&detail::use_any_classes< + Registry, boost::type_erasure::any, std::decay_t>; + + if constexpr (std::is_rvalue_reference_v) { + return std::move( + boost::type_erasure::any_cast&>( + arg)); + } else if constexpr ( + !std::is_reference_v && detail::te_owning) { + return U(std::move(boost::type_erasure::any_cast(arg))); + } else { + return boost::type_erasure::any_cast(arg); + } + } + } +}; + +//! Specialize virtual_traits for the mutable any reference, +//! `boost::type_erasure::any`, passed by value. +//! +//! The any references are cheap, two-word handles; passing them by +//! value is the idiomatic way to use them as parameters. Dispatch is on +//! the type *bound at construction*, obtained via +//! `boost::type_erasure::typeid_of` - not the C++ RTTI dynamic type of +//! the referent. `Concept` must contain `boost::type_erasure::typeid_<>`; +//! `relaxed` implies it. +//! +//! @tparam C The `any`'s Concept. +//! @tparam T The referent placeholder (`_self` for `any`). +//! @tparam Registry A @ref registry. +//! +//! @see [Interoperation with Boost.TypeErasure](xref:ROOT:interop_type_erasure.adoc) +template +struct virtual_traits, Registry> { + //! The type used for dispatch: the owning `any` for `C`. + using virtual_type = boost::type_erasure::any; + + //! Returns a const reference to the `any` argument. + //! @param arg A reference to an `any`. + //! @return A const reference to `arg`. + static auto peek(const boost::type_erasure::any& arg) + -> const boost::type_erasure::any& { + return arg; + } + + //! Returns a *reference* to a v-table pointer for the bound value. + //! + //! Looks up the @ref type_id returned by + //! `boost::type_erasure::typeid_of` in the registry's `vptr` policy. + //! + //! This requires the registry's @ref rtti policy to be @ref std_rtti; + //! the requirement is enforced with a `static_assert`. Registers the + //! owning `any` - the root class of the bound types - in `Registry`. + //! + //! @param arg A reference to a const `any`. + //! @return A reference to the v-table pointer for the bound value. + static auto + vptr(const boost::type_erasure::any& arg) -> const vptr_type& { + detail::assert_std_rtti_type_erasure(); + (void)&detail::use_any_classes>; + return Registry::vptr::vptr(&boost::type_erasure::typeid_of(arg)); + } + + //! Cast to a type. + //! + //! Extracts the referent using `boost::type_erasure::any_cast`, and + //! registers `U`, stripped of reference and cv-qualifiers, in + //! `Registry` as a class derived from the owning `any`. + //! Supports mutable references (e.g. `Dog&`); modifications through + //! the result are visible through the referent. `U` cannot be an + //! rvalue reference - the referent is not owned; the overloads are + //! removed from the overload set. + //! + //! @tparam U The target type (e.g. `Dog&`, `const Dog&`, `Dog`). + //! @param arg The any reference method argument. + //! @return The value bound to `arg`, cast to `U`. + template< + typename U, typename = std::enable_if_t>> + static auto cast(boost::type_erasure::any arg) -> decltype(auto) { + if constexpr (detail::te_pass_through< + U, boost::type_erasure::any>) { + // by value: a reference would dangle when this function's + // parameter goes out of scope + return arg; + } else { + (void)&detail::use_any_classes< + Registry, boost::type_erasure::any, std::decay_t>; + return boost::type_erasure::any_cast(arg); + } + } +}; + +//! Specialize virtual_traits for the const any reference, +//! `boost::type_erasure::any`, passed by value. +//! +//! The any references are cheap, two-word handles; passing them by +//! value is the idiomatic way to use them as parameters. Dispatch is on +//! the type *bound at construction*, obtained via +//! `boost::type_erasure::typeid_of` - not the C++ RTTI dynamic type of +//! the referent. `Concept` must contain `boost::type_erasure::typeid_<>`; +//! `relaxed` implies it. +//! +//! @tparam C The `any`'s Concept. +//! @tparam T The referent placeholder (`_self` for +//! `any`). +//! @tparam Registry A @ref registry. +//! +//! @see [Interoperation with Boost.TypeErasure](xref:ROOT:interop_type_erasure.adoc) +template +struct virtual_traits, Registry> { + //! The type used for dispatch: the owning `any` for `C`. + using virtual_type = boost::type_erasure::any; + + //! Returns a const reference to the `any` argument. + //! @param arg A reference to an `any`. + //! @return A const reference to `arg`. + static auto peek(const boost::type_erasure::any& arg) + -> const boost::type_erasure::any& { + return arg; + } + + //! Returns a *reference* to a v-table pointer for the bound value. + //! + //! Looks up the @ref type_id returned by + //! `boost::type_erasure::typeid_of` in the registry's `vptr` policy. + //! + //! This requires the registry's @ref rtti policy to be @ref std_rtti; + //! the requirement is enforced with a `static_assert`. Registers the + //! owning `any` - the root class of the bound types - in `Registry`. + //! + //! @param arg A reference to a const `any`. + //! @return A reference to the v-table pointer for the bound value. + static auto + vptr(const boost::type_erasure::any& arg) -> const vptr_type& { + detail::assert_std_rtti_type_erasure(); + (void)&detail::use_any_classes>; + return Registry::vptr::vptr(&boost::type_erasure::typeid_of(arg)); + } + + //! Cast to a type. + //! + //! Extracts the referent using `boost::type_erasure::any_cast`, and + //! registers `U`, stripped of reference and cv-qualifiers, in + //! `Registry` as a class derived from the owning `any`. Since + //! the referent is const, `U` must be a value or a const reference; + //! the other overloads are removed from the overload set. + //! + //! @tparam U The target type (e.g. `const Dog&`, `Dog`). + //! @param arg The any reference method argument. + //! @return The value bound to `arg`, cast to `U`. + template< + typename U, + typename = std::enable_if_t< + !std::is_rvalue_reference_v && !detail::te_mutable_target>> + static auto + cast(boost::type_erasure::any arg) -> decltype(auto) { + if constexpr (detail::te_pass_through< + U, boost::type_erasure::any>) { + // by value: a reference would dangle when this function's + // parameter goes out of scope + return arg; + } else { + (void)&detail::use_any_classes< + Registry, boost::type_erasure::any, std::decay_t>; + return boost::type_erasure::any_cast(arg); + } + } +}; + +//! A Boost.TypeErasure concept that makes an `any` intrinsically +//! polymorphic. +//! +//! Including `openmethod_vptr` in a Concept adds an operation, +//! to the dispatch table of every `any` on `Concept`, that returns +//! the @ref registry::static_vptr for the bound type; and it surfaces the +//! operation as a @ref boost_openmethod_vptr overload, which dispatch +//! prefers over the registry's `vptr` policy. Calls thus resolve in +//! constant time, without hashing the result of +//! `boost::type_erasure::typeid_of`. +//! +//! In addition, binding a value to such an `any` registers its type as a +//! class derived from the owning `any` - the same shape that naming the +//! type as an overrider parameter produces, with which it can coexist. +//! +//! `Concept` must be the very Concept the `any` is instantiated with. +//! Since the concept appears inside that Concept, the Concept must name +//! itself: define it as a struct deriving from the concept list. +//! +//! Unlike the `vptr` policy, which reports a @ref missing_class error, +//! calling a method on an empty relaxed `any` throws +//! `boost::type_erasure::bad_function_call`. +//! +//! An `any` that carries this concept cannot be wrapped in a +//! @ref virtual_any: the hook returns the v-table pointer by value, and +//! an indirect registry cannot store that. Wrapping one is rejected at +//! compile time. +//! +//! Both give constant-time access to the v-table pointer, but not at the +//! same cost. This concept reaches it through an indirect call on the +//! `any`'s own dispatch table; a `virtual_any` loads it from the wrapper. +//! What the concept saves over a plain `any` is the hash of +//! `boost::type_erasure::typeid_of`, not the call. +//! +//! @tparam Concept The Concept containing this concept. +//! @tparam Registry A @ref registry. +//! @tparam T A placeholder; leave it to its default, `_self`. +//! +//! @par Example +//! include:type_erasure_concept.cpp#concept +//! +//! @see [Interoperation with Boost.TypeErasure](xref:ROOT:interop_type_erasure.adoc) +template< + class Concept, class Registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY, + typename T = boost::type_erasure::_self> +struct openmethod_vptr { + //! Returns the v-table pointer for the bound type. + //! + //! Also registers `T`, and the owning `any` for `Concept` as its + //! base, by odr-using their registrars. + //! + //! @return The @ref registry::static_vptr for `T`. + static auto apply(const T&) -> vptr_type { + (void)&detail::use_any_classes< + Registry, boost::type_erasure::any, T>; + return Registry::template static_vptr; + } +}; + +} // namespace boost::openmethod + +namespace boost::type_erasure { + +// Surface the openmethod_vptr operation as the boost_openmethod_vptr +// intrinsic hook, injected into the interface of every any whose +// Concept contains the concept. +template +struct concept_interface< + boost::openmethod::openmethod_vptr, Base, T> : Base { + // The parameter is a deduced `Self`, constrained to the exact any + // type, because MSVC's `/std:c++17` does not imply `/permissive-`: + // it injects hidden friends into the enclosing namespace, where + // ordinary lookup finds them. A `const derived::type&` + // parameter would then make this a candidate for an any over an + // unrelated Concept, which MSVC tries to convert to this one - and + // the conversion fails outside the immediate context, so it is an + // error, not a substitution failure. + template + friend auto boost_openmethod_vptr(const Self& arg, Registry*) + -> std::enable_if_t< + std::is_same_v::type>, + boost::openmethod::vptr_type> { + return call( + boost::openmethod::openmethod_vptr(), arg); + } +}; + +} // namespace boost::type_erasure + +namespace boost::openmethod { + +// The primary final_virtual_ptr would silently use the static v-table +// pointer of the any class itself - the root -, not the bound value's. +// Delete the combination. Both call forms need covering: the (C, T)-only +// templates catch calls that deduce the default registry, and the +// Registry-first templates catch explicit-registry calls; both are more +// specialized than the primary's forwarding-reference parameter. +// +// Hidden from the reference: they are a guard, not API, and six deleted +// overloads would crowd the `final_virtual_ptr` overload list. + +#ifndef __MRDOCS__ +template +void final_virtual_ptr(const boost::type_erasure::any&) = delete; +template +void final_virtual_ptr(boost::type_erasure::any&) = delete; +template +void final_virtual_ptr(boost::type_erasure::any&&) = delete; +template +void final_virtual_ptr(const boost::type_erasure::any&) = delete; +template +void final_virtual_ptr(boost::type_erasure::any&) = delete; +template +void final_virtual_ptr(boost::type_erasure::any&&) = delete; +#endif + +namespace aliases { +using boost::openmethod::openmethod_vptr; +} // namespace aliases + +} // namespace boost::openmethod + +#endif diff --git a/include/boost/openmethod/interop/std_any.hpp b/include/boost/openmethod/interop/std_any.hpp new file mode 100644 index 00000000..ca6113ac --- /dev/null +++ b/include/boost/openmethod/interop/std_any.hpp @@ -0,0 +1,302 @@ +// Copyright (c) 2018-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_STD_ANY_HPP +#define BOOST_OPENMETHOD_INTEROP_STD_ANY_HPP + +#include +#include +#include +#include + +// Dispatch on the type contained in a `std::any`. +// +// This interop is based on a design contributed by Steven Watanabe: +// https://github.com/boostorg/openmethod/issues/21 + +namespace boost::openmethod { + +namespace detail { + +template +struct validate_method_parameter, Registry, void> + : std::true_type {}; + +template +struct validate_method_parameter, Registry, void> + : std::true_type {}; + +template +struct validate_method_parameter, 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 +// other policy the lookup key is meaningless, and `type_id` being +// `const void*`, nothing would diagnose it. +template +constexpr void assert_std_rtti_std_any() { + static_assert( + std::is_base_of_v< + policies::std_rtti, + find_first_derived_of< + policies::rtti, typename Registry::policy_list>>, + "requires standard RTTI"); +} + +} // namespace detail + +//! Specialize virtual_traits for `const std::any&` (const reference). +//! +//! Dispatch is based on the runtime type of the value stored in the `any`, +//! obtained via `std::any::type()`. +//! +//! @tparam Registry A @ref registry. +template +struct virtual_traits { + //! The type used for dispatch. + using virtual_type = std::any; + + //! Returns a const reference to the `any` argument. + //! @param arg A reference to a `std::any`. + //! @return A const reference to `arg`. + static auto peek(const std::any& arg) -> const std::any& { + return arg; + } + + //! Returns a *reference* to a v-table pointer for an object. + //! + //! Acquires the @ref type_id of the value stored in `arg`, using + //! `std::any::type()`. This requires the registry's @ref rtti policy to be + //! @ref std_rtti; the requirement is enforced with a `static_assert`. + //! Registers `std::any` - the root class of the contained types - in + //! `Registry`. + //! + //! Passes it to the registry's @ref policies::vptr policy, which must + //! provide @ref policies::VptrFn::vptr. Both @ref policies::vptr_vector + //! and @ref policies::vptr_map do. + //! + //! If the registry has a @ref type_hash policy, uses it to convert the + //! type id to an index; otherwise, uses the type_id as the index. + //! + //! If the registry contains the @ref runtime_checks policy, verifies + //! that the index falls within the limits of the vector. If it does + //! not, and if the registry contains a @ref error_handler policy, calls + //! its @ref error function with a @ref missing_class value, then + //! terminates the program with @ref abort. + //! + //! @param arg A reference to a const `any`. + //! @return A reference to the v-table pointer for the stored value. + static auto vptr(const std::any& arg) -> const vptr_type& { + detail::assert_std_rtti_std_any(); + (void)&detail::use_any_classes; + return Registry::vptr::vptr(&arg.type()); + } + + //! Cast to a type. + //! + //! If `U` is the `any` itself (by any reference category), returns + //! `arg` unchanged, which is how a catch-all overrider is written. + //! Otherwise, extracts the stored value using `std::any_cast`, and + //! registers `U`, stripped of reference and cv-qualifiers, in + //! `Registry` as a class derived from `std::any`. Since the `any` + //! argument is const, `U` cannot be a mutable reference. + //! + //! @tparam U The target type (e.g. `const Dog&`, `Dog`). + //! @param arg A reference to a const `std::any` method argument. + //! @return The value stored in `arg`, cast to `U`. + template + static auto cast(const std::any& arg) -> decltype(auto) { + if constexpr (std::is_same_v< + std::remove_cv_t>, + std::any>) { + return (arg); + } else { + (void)&detail::use_any_classes>; + return std::any_cast(arg); + } + } +}; + +//! Specialize virtual_traits for `std::any&` (mutable reference). +//! +//! Dispatch is based on the runtime type of the value stored in the `any`, +//! obtained via `std::any::type()`. +//! +//! @tparam Registry A @ref registry. +template +struct virtual_traits { + //! The type used for dispatch. + using virtual_type = std::any; + + //! Returns a const reference to the `any` argument. + //! @param arg A reference to a `std::any`. + //! @return A const reference to `arg`. + static auto peek(const std::any& arg) -> const std::any& { + return arg; + } + + //! Returns a *reference* to a v-table pointer for an object. + //! + //! Acquires the @ref type_id of the value stored in `arg`, using + //! `std::any::type()`. This requires the registry's @ref rtti policy to be + //! @ref std_rtti; the requirement is enforced with a `static_assert`. + //! Registers `std::any` - the root class of the contained types - in + //! `Registry`. + //! + //! Passes it to the registry's @ref policies::vptr policy, which must + //! provide @ref policies::VptrFn::vptr. Both @ref policies::vptr_vector + //! and @ref policies::vptr_map do. + //! + //! If the registry has a @ref type_hash policy, uses it to convert the + //! type id to an index; otherwise, uses the type_id as the index. + //! + //! If the registry contains the @ref runtime_checks policy, verifies + //! that the index falls within the limits of the vector. If it does + //! not, and if the registry contains a @ref error_handler policy, calls + //! its @ref error function with a @ref missing_class value, then + //! terminates the program with @ref abort. + //! + //! @param arg A reference to a `std::any`. + //! @return A reference to the v-table pointer for the stored value. + static auto vptr(const std::any& arg) -> const vptr_type& { + detail::assert_std_rtti_std_any(); + (void)&detail::use_any_classes; + return Registry::vptr::vptr(&arg.type()); + } + + //! Cast to a type. + //! + //! If `U` is the `any` itself, returns `arg` unchanged, which is how a + //! catch-all overrider is written. Otherwise, extracts the stored value + //! using `std::any_cast`, and registers `U`, stripped of reference and + //! cv-qualifiers, in `Registry` as a class derived from `std::any`. + //! Supports mutable references (e.g. `Dog&`) because the `any` argument + //! is not const; modifications through the result are visible through + //! the `any`. + //! + //! @tparam U The target type (e.g. `Dog&`, `const Dog&`, `Dog`). + //! @param arg A mutable reference to the `std::any` method argument. + //! @return The value stored in `arg`, cast to `U`. + template + static auto cast(std::any& arg) -> decltype(auto) { + if constexpr (std::is_same_v< + std::remove_cv_t>, + std::any>) { + return (arg); + } else { + (void)&detail::use_any_classes>; + return std::any_cast(arg); + } + } +}; + +//! Specialize virtual_traits for `std::any&&` (xvalue reference). +//! +//! Dispatch is based on the runtime type of the value stored in the `any`, +//! obtained via `std::any::type()`. +//! +//! @tparam Registry A @ref registry. +template +struct virtual_traits { + //! The type used for dispatch. + using virtual_type = std::any; + + //! Returns a const reference to the `any` argument. + //! @param arg A reference to a `std::any`. + //! @return A const reference to `arg`. + static auto peek(const std::any& arg) -> const std::any& { + return arg; + } + + //! Returns a *reference* to a v-table pointer for an object. + //! + //! Acquires the @ref type_id of the value stored in `arg`, using + //! `std::any::type()`. This requires the registry's @ref rtti policy to be + //! @ref std_rtti; the requirement is enforced with a `static_assert`. + //! Registers `std::any` - the root class of the contained types - in + //! `Registry`. + //! + //! Passes it to the registry's @ref policies::vptr policy, which must + //! provide @ref policies::VptrFn::vptr. Both @ref policies::vptr_vector + //! and @ref policies::vptr_map do. + //! + //! If the registry has a @ref type_hash policy, uses it to convert the + //! type id to an index; otherwise, uses the type_id as the index. + //! + //! If the registry contains the @ref runtime_checks policy, verifies + //! that the index falls within the limits of the vector. If it does + //! not, and if the registry contains a @ref error_handler policy, calls + //! its @ref error function with a @ref missing_class value, then + //! terminates the program with @ref abort. + //! + //! @param arg A reference to a const `any`. + //! @return A reference to the v-table pointer for the stored value. + static auto vptr(const std::any& arg) -> const vptr_type& { + detail::assert_std_rtti_std_any(); + (void)&detail::use_any_classes; + return Registry::vptr::vptr(&arg.type()); + } + + //! Cast to a type. + //! + //! If `U` is the `any` itself, returns `arg` unchanged, which is how a + //! catch-all overrider is written. Otherwise, extracts the stored value + //! using `std::any_cast`, and registers `U`, stripped of reference and + //! cv-qualifiers, in `Registry` as a class derived from `std::any`. + //! + //! @tparam U The target type (e.g. `Dog&&`, `const Dog&`, `Dog`). + //! @param arg An rvalue reference to the `std::any` method argument. + //! @return The value stored in `arg`, cast to `U`. + template + static auto cast(std::any&& arg) -> decltype(auto) { + if constexpr (std::is_same_v< + std::remove_cv_t>, + std::any>) { + return std::move(arg); + } else { + (void)&detail::use_any_classes>; + return std::any_cast(std::move(arg)); + } + } +}; + +//! Alias for a `virtual_any`, in the default registry. +//! +//! With another registry, use `virtual_any` directly. +//! +//! @see [Interoperation with `any`](xref:ROOT:interop_any.adoc) +using virtual_std_any = virtual_any; + +// The primary final_virtual_ptr would silently use static_vptr +// - the v-table of the `any` root class, not of the contained value. +// Delete the combination. Both call forms need covering: the non-template +// overloads catch calls that deduce the default registry, and are removed +// from consideration when an explicit template argument list is given, so +// the Registry-only templates - more specialized than the primary - catch +// those. +// +// Hidden from the reference: they are a guard, not API, and six deleted +// overloads would crowd the `final_virtual_ptr` overload list. + +#ifndef __MRDOCS__ +template +void final_virtual_ptr(const std::any&) = delete; +template +void final_virtual_ptr(std::any&) = delete; +template +void final_virtual_ptr(std::any&&) = delete; +void final_virtual_ptr(const std::any&) = delete; +void final_virtual_ptr(std::any&) = delete; +void final_virtual_ptr(std::any&&) = delete; +#endif + +namespace aliases { +using boost::openmethod::virtual_std_any; +} // namespace aliases + +} // namespace boost::openmethod + +#endif diff --git a/include/boost/openmethod/interop/virtual_any.hpp b/include/boost/openmethod/interop/virtual_any.hpp new file mode 100644 index 00000000..17a3708a --- /dev/null +++ b/include/boost/openmethod/interop/virtual_any.hpp @@ -0,0 +1,626 @@ +// Copyright (c) 2018-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_VIRTUAL_ANY_HPP +#define BOOST_OPENMETHOD_INTEROP_VIRTUAL_ANY_HPP + +#include + +#include +#include + +namespace boost::openmethod { + +template +class virtual_any; + +BOOST_OPENMETHOD_OPEN_NAMESPACE_DETAIL_UNLESS_MRDOCS + +//! Test if argument is a wide `any` (exposition only) +//! +//! Evaluates to `true` if `T` is a specialization of @ref virtual_any, and +//! `false` otherwise. +//! +//! This constrains the constructor and the assignment operator of +//! @ref virtual_any that take a value, excluding every specialization of the +//! wide type, not only the one matching this `virtual_any`. A matching +//! argument then selects the copy or move operation instead of being stored +//! inside the `any`; any other specialization is rejected outright rather +//! than stored: a `virtual_any` is not a registered class, so its +//! @ref registry::static_vptr would be null. +//! +//! @tparam T A type. +template +constexpr bool IsVirtualAny = false; + +//! Recognize a virtual_any (exposition only) +//! +//! The specialization of @ref IsVirtualAny that matches a +//! `virtual_any`, and evaluates to `true`. +//! +//! @tparam Any An `any` type. +//! @tparam Registry A @ref registry. +template +constexpr bool IsVirtualAny> = true; + +BOOST_OPENMETHOD_CLOSE_NAMESPACE_DETAIL_UNLESS_MRDOCS + +namespace detail { + +// Registers Class under the `any` root class, plus the root itself. +// odr-used by the any interop's virtual_traits - cast (one instantiation +// per overrider parameter) and vptr (root only), on both the plain `any` +// and the virtual_any specializations - and by virtual_any's value +// constructor, value assignment and emplace. Naming a type as an overrider +// parameter, or storing a value in a virtual_any, thus registers it +// automatically; dispatching on a virtual_any registers the root, because +// that goes through vptr. mp_unique collapses the Class == Root case to the +// root entry alone. +template +inline boost::mp11::mp_apply< + tuple, + boost::mp11::mp_transform_q< + boost::mp11::mp_bind_front, + boost::mp11::mp_unique, + boost::mp11::mp_list>>>> + use_any_classes; + +} // namespace detail + +//! A wide `any`, combining an `any` and a pointer to a v-table. +//! +//! `virtual_any` is to `any` what @ref virtual_ptr is to a pointer: it +//! carries the v-table pointer for the value stored in the `any`, so +//! methods dispatch on the contained type without looking it up on every +//! call. Unlike `virtual_ptr`, it *owns* its object: the `any` is held by +//! value. +//! +//! The v-table pointer is acquired when the `virtual_any` is created: +//! either from the dynamic type of an existing `any` (a hash table +//! lookup, via `virtual_traits::vptr`), or +//! statically, when the contained type is known at compile time (the +//! value constructor and @ref emplace use @ref registry::static_vptr). +//! +//! Contained types are registered automatically, as classes derived from +//! `Any`: naming a type as the parameter of an overrider - or storing a +//! value in a `virtual_any` - registers it in `Registry`. +//! +//! Methods take `virtual_any` parameters by reference: `const +//! virtual_any&`, `virtual_any&` or `virtual_any&&`. Overriders receive +//! the *contained* type, by a reference of a compatible category - or the +//! `virtual_any` itself, unchanged, for a catch-all overrider. +//! +//! The contained value cannot be replaced through a `virtual_any` other +//! than via assignment or @ref emplace, which re-derive the v-table +//! pointer, thus maintaining the invariant that the v-table pointer +//! corresponds to the contained type. +//! +//! `Any` can be `std::any`, `boost::any`, or any type that has an +//! `any`-like interface, and specializes `virtual_traits` for its +//! reference types, providing `vptr` and `cast`. +//! +//! @tparam Any An `any` type. +//! @tparam Registry A @ref registry. +//! +//! @par Example +//! include:virtual_any.cpp#classes;method;dispatch +//! +//! @see [Interoperation with `any`](xref:ROOT:interop_any.adoc) +template +class virtual_any { + static constexpr bool use_indirect_vptrs = Registry::has_indirect_vptr; + + Any obj; + std::conditional_t vp; + + template + friend struct virtual_traits; + + // The v-table pointer, by reference, for virtual_traits::vptr, which + // must return one so that the caller observes a re-initialize. An + // indirect registry stores the address of the cell holding it; a direct + // one stores it outright, in a member that outlives the call. + auto vptr_ref() const -> const vptr_type& { + if constexpr (use_indirect_vptrs) { + return *vp; + } else { + return vp; + } + } + + public: + //! Construct an empty `virtual_any`. + //! + //! The `any` is empty, and the v-table pointer is null. + virtual_any() + : obj(), vp(detail::box_vptr(detail::null_vptr)) { + } + + //! Construct from an `any` (copy). + //! + //! Copies `other`, and acquires the v-table pointer for the contained + //! value, using `virtual_traits::vptr`. + //! + //! @param other An `any`. + //! + //! @par Example + //! include:virtual_any.cpp#from_any + virtual_any(const Any& other) + : obj(other), vp(detail::box_vptr( + detail::acquire_vptr(obj))) { + } + + //! Construct from an `any` (move). + //! + //! Moves `other`, and acquires the v-table pointer for the contained + //! value, using `virtual_traits::vptr`. + //! + //! @param other An `any`. + virtual_any(Any&& other) + : obj(std::move(other)), vp(detail::box_vptr( + detail::acquire_vptr(obj))) { + } + + //! Construct from a value. + //! + //! Stores `value` in the `any`, and sets the v-table pointer to the + //! @ref registry::static_vptr for its type - no hash table lookup is + //! involved. The type of `value`, stripped from reference and + //! cv-qualifiers, is registered automatically in `Registry`, as a + //! class derived from `Any`. + //! + //! @tparam T The type of the value. + //! @param value The value to store. + //! + //! @par Example + //! include:virtual_any.cpp#from_value + template< + typename T, + typename = std::enable_if_t< + !BOOST_OPENMETHOD_DETAIL_UNLESS_MRDOCS + IsVirtualAny> && + !std::is_same_v, Any> && + std::is_constructible_v>> + virtual_any(T&& value) + : obj(std::forward(value)), + vp(detail::box_vptr( + Registry::template static_vptr>)) { + (void)&detail::use_any_classes>; + Registry::require_initialized(); + BOOST_ASSERT(detail::unbox_vptr(vp) != nullptr); + } + + //! Copy constructor. + virtual_any(const virtual_any& other) = default; + + //! Move constructor. + //! + //! Moves the `any`, and sets `other`'s v-table pointer to null. + //! + //! @param other A `virtual_any`. + virtual_any(virtual_any&& other) : obj(std::move(other.obj)), vp(other.vp) { + other.vp = detail::box_vptr(detail::null_vptr); + } + + //! Copy assignment operator. + auto operator=(const virtual_any& other) -> virtual_any& = default; + + //! Move assignment operator. + //! + //! Moves the `any`, and sets `other`'s v-table pointer to null. + //! + //! @param other A `virtual_any`. + auto operator=(virtual_any&& other) -> virtual_any& { + obj = std::move(other.obj); + vp = other.vp; + other.vp = detail::box_vptr(detail::null_vptr); + return *this; + } + + //! Assign from an `any` (copy). + //! + //! Copies `other`, and re-acquires the v-table pointer for the + //! contained value. + //! + //! @param other An `any`. + auto operator=(const Any& other) -> virtual_any& { + obj = other; + vp = detail::box_vptr( + detail::acquire_vptr(obj)); + return *this; + } + + //! Assign from an `any` (move). + //! + //! Moves `other`, and re-acquires the v-table pointer for the + //! contained value. + //! + //! @param other An `any`. + auto operator=(Any&& other) -> virtual_any& { + obj = std::move(other); + vp = detail::box_vptr( + detail::acquire_vptr(obj)); + return *this; + } + + //! Assign from a value. + //! + //! Stores `value` in the `any`, and sets the v-table pointer to the + //! @ref registry::static_vptr for its type - no hash table lookup is + //! involved. The type of `value`, stripped from reference and + //! cv-qualifiers, is registered automatically in `Registry`, as a + //! class derived from `Any`. + //! + //! @tparam T The type of the value. + //! @param value The value to store. + template< + typename T, + typename = std::enable_if_t< + !BOOST_OPENMETHOD_DETAIL_UNLESS_MRDOCS + IsVirtualAny> && + !std::is_same_v, Any> && + std::is_constructible_v>> + auto operator=(T&& value) -> virtual_any& { + (void)&detail::use_any_classes>; + obj = std::forward(value); + Registry::require_initialized(); + vp = detail::box_vptr( + Registry::template static_vptr>); + BOOST_ASSERT(detail::unbox_vptr(vp) != nullptr); + return *this; + } + + //! Construct a value in place. + //! + //! Stores a `Class` constructed from `args`, and sets the v-table + //! pointer to the @ref registry::static_vptr for `Class` - no hash + //! table lookup is involved. `Class` is registered automatically in + //! `Registry`, as a class derived from `Any`. + //! + //! @tparam Class The type of the value to construct. + //! @tparam T Types of the arguments to pass to the constructor. + //! @param args Arguments to pass to the constructor of `Class`. + //! + //! @par Example + //! include:virtual_any.cpp#emplace + template + auto emplace(T&&... args) -> void { + (void)&detail::use_any_classes; + obj = Class(std::forward(args)...); + Registry::require_initialized(); + vp = detail::box_vptr( + Registry::template static_vptr); + BOOST_ASSERT(detail::unbox_vptr(vp) != nullptr); + } + + //! Return a reference to the (non-modifiable) `any`. + auto get() const -> const Any& { + return obj; + } + + //! Return the v-table pointer. + auto vptr() const -> vptr_type { + return detail::unbox_vptr(vp); + } +}; + +//! Specialize virtual_traits for `const virtual_any&`. +//! +//! Dispatch is on the v-table pointer stored in the `virtual_any`. +//! +//! @tparam Any An `any` type. +//! @tparam Registry A @ref registry. +template +struct virtual_traits&, Registry> { + //! The type used for dispatch. + using virtual_type = Any; + + //! Returns a const reference to the `virtual_any` argument. + //! @param arg A reference to a `virtual_any`. + //! @return A const reference to `arg`. + static auto peek(const virtual_any& arg) + -> const virtual_any& { + return arg; + } + + //! Returns a *reference* to the v-table pointer for the stored value. + //! + //! The `virtual_any` acquired the v-table pointer when it was created, + //! so no lookup is involved. Registers `Any` - the root class of the + //! contained types - in `Registry`. + //! + //! @param arg A reference to a const `virtual_any`. + //! @return A reference to the v-table pointer for the stored value. + static auto + vptr(const virtual_any& arg) -> const vptr_type& { + (void)&detail::use_any_classes; + return arg.vptr_ref(); + } + + //! Cast to a type. + //! + //! If `U` is the `virtual_any` itself (by any reference category), + //! returns `arg` unchanged. Otherwise, extracts the stored value + //! using `virtual_traits::cast`, and registers + //! `U`, stripped of reference and cv-qualifiers, in `Registry` as a + //! class derived from `Any`. Since the `any` is not modifiable, `U` + //! cannot be a mutable reference. + //! + //! @tparam U The target type (e.g. `const Dog&`, `Dog`). + //! @param arg A reference to a const `virtual_any` method argument. + //! @return The value stored in `arg`, cast to `U`. + template + static auto cast(const virtual_any& arg) -> decltype(auto) { + if constexpr (std::is_same_v< + std::remove_cv_t>, + virtual_any>) { + return (arg); + } else { + (void)&detail::use_any_classes>; + return virtual_traits::template cast( + arg.obj); + } + } +}; + +//! Specialize virtual_traits for `virtual_any&` (mutable reference). +//! +//! Dispatch is on the v-table pointer stored in the `virtual_any`. +//! +//! @tparam Any An `any` type. +//! @tparam Registry A @ref registry. +template +struct virtual_traits&, Registry> { + //! The type used for dispatch. + using virtual_type = Any; + + //! Returns a const reference to the `virtual_any` argument. + //! @param arg A reference to a `virtual_any`. + //! @return A const reference to `arg`. + static auto peek(const virtual_any& arg) + -> const virtual_any& { + return arg; + } + + //! Returns a *reference* to the v-table pointer for the stored value. + //! + //! The `virtual_any` acquired the v-table pointer when it was created, + //! so no lookup is involved. Registers `Any` - the root class of the + //! contained types - in `Registry`. + //! + //! @param arg A reference to a `virtual_any`. + //! @return A reference to the v-table pointer for the stored value. + static auto + vptr(const virtual_any& arg) -> const vptr_type& { + (void)&detail::use_any_classes; + return arg.vptr_ref(); + } + + //! Cast to a type. + //! + //! If `U` is the `virtual_any` itself (by mutable reference), returns + //! `arg` unchanged. Otherwise, extracts the stored value using + //! `virtual_traits::cast`, and registers `U`, + //! stripped of reference and cv-qualifiers, in `Registry` as a class + //! derived from `Any`. Supports mutable references (e.g. `Dog&`); + //! modifications through the result are visible through the + //! `virtual_any`. + //! + //! @tparam U The target type (e.g. `Dog&`, `const Dog&`, `Dog`). + //! @param arg A mutable reference to the `virtual_any` method + //! argument. + //! @return The value stored in `arg`, cast to `U`. + template + static auto cast(virtual_any& arg) -> decltype(auto) { + if constexpr (std::is_same_v< + std::remove_cv_t>, + virtual_any>) { + return (arg); + } else { + (void)&detail::use_any_classes>; + return virtual_traits::template cast(arg.obj); + } + } +}; + +//! Specialize virtual_traits for `virtual_any&&` (xvalue reference). +//! +//! Dispatch is on the v-table pointer stored in the `virtual_any`. +//! +//! @tparam Any An `any` type. +//! @tparam Registry A @ref registry. +template +struct virtual_traits&&, Registry> { + //! The type used for dispatch. + using virtual_type = Any; + + //! Returns a const reference to the `virtual_any` argument. + //! @param arg A reference to a `virtual_any`. + //! @return A const reference to `arg`. + static auto peek(const virtual_any& arg) + -> const virtual_any& { + return arg; + } + + //! Returns a *reference* to the v-table pointer for the stored value. + //! + //! The `virtual_any` acquired the v-table pointer when it was created, + //! so no lookup is involved. Registers `Any` - the root class of the + //! contained types - in `Registry`. + //! + //! @param arg A reference to a `virtual_any`. + //! @return A reference to the v-table pointer for the stored value. + static auto + vptr(const virtual_any& arg) -> const vptr_type& { + (void)&detail::use_any_classes; + return arg.vptr_ref(); + } + + //! Cast to a type. + //! + //! If `U` is the `virtual_any` itself (by rvalue reference), returns + //! `arg` unchanged. Otherwise, extracts the stored value using + //! `virtual_traits::cast`, and registers `U`, + //! stripped of reference and cv-qualifiers, in `Registry` as a class + //! derived from `Any`. + //! + //! @tparam U The target type (e.g. `Dog&&`, `const Dog&`, `Dog`). + //! @param arg An rvalue reference to the `virtual_any` method + //! argument. + //! @return The value stored in `arg`, cast to `U`. + template + static auto cast(virtual_any&& arg) -> decltype(auto) { + if constexpr (std::is_same_v< + std::remove_cv_t>, + virtual_any>) { + return std::move(arg); + } else { + (void)&detail::use_any_classes>; + return virtual_traits::template cast( + std::move(arg.obj)); + } + } +}; + +//! Reject wrapping a `virtual_any` in a @ref virtual_ptr. +//! +//! A `virtual_any` is already a wide type: it carries the v-table pointer +//! for the value it contains. Wrapping it in a `virtual_ptr` would produce +//! a wide pointer whose v-table is the contained value's, but whose static +//! type is the `virtual_any` - which is deliberately not a registered +//! class. This specialization rejects the combination at compile time, +//! which also covers @ref final_virtual_ptr, since that instantiates the +//! `virtual_ptr` it returns. +//! +//! @tparam Class A specialization of @ref virtual_any, possibly const. +//! @tparam Registry A @ref registry. +template +class virtual_ptr< + Class, Registry, + std::enable_if_t>>> { + static_assert( + detail::false_t, + "do not wrap a virtual_any in a virtual_ptr: it already carries the " + "v-table pointer for the value it contains; pass it by reference " + "instead"); +}; + +namespace detail { + +template +struct is_virtual&> : std::true_type {}; + +template +struct is_virtual&> : std::true_type {}; + +template +struct is_virtual&&> : std::true_type {}; + +template +struct parameter_traits&, Registry> + : virtual_traits&, Registry> {}; + +template +struct parameter_traits&, Registry> + : virtual_traits&, Registry> {}; + +template +struct parameter_traits&&, Registry> + : virtual_traits&&, Registry> {}; + +template +struct validate_method_parameter< + virtual_any, MethodRegistry, void> : std::false_type { + static_assert( + false_t, "virtual_any must be passed by reference"); +}; + +template +struct validate_method_parameter< + virtual_any&, MethodRegistry, void> + : std::bool_constant> { + static_assert( + std::is_same_v, "registry mismatch"); +}; + +template +struct validate_method_parameter< + const virtual_any&, MethodRegistry, void> + : std::bool_constant> { + static_assert( + std::is_same_v, "registry mismatch"); +}; + +template +struct validate_method_parameter< + virtual_any&&, MethodRegistry, void> + : std::bool_constant> { + static_assert( + std::is_same_v, "registry mismatch"); +}; + +// A virtual_any method parameter places no compile-time constraint on the +// corresponding overrider parameter: the adjustment is delegated entirely +// to virtual_traits::cast, like for virtual_ +// parameters. The exact-pair specializations disambiguate with the +// generic specialization in core.hpp, which is neither more nor +// less specialized than . + +template +struct validate_overrider_parameter&, T2, void> + : std::true_type {}; + +template +struct validate_overrider_parameter< + virtual_any&, virtual_any&, void> + : std::true_type {}; + +template +struct validate_overrider_parameter&, T2, void> + : std::true_type {}; + +template +struct validate_overrider_parameter< + const virtual_any&, const virtual_any&, void> + : std::true_type {}; + +template +struct validate_overrider_parameter&&, T2, void> + : std::true_type {}; + +template +struct validate_overrider_parameter< + virtual_any&&, virtual_any&&, void> + : std::true_type {}; + +template +struct select_overrider_virtual_type_aux< + virtual_any&, Q, Registry> { + using type = virtual_type; +}; + +template +struct select_overrider_virtual_type_aux< + const virtual_any&, Q, Registry> { + using type = virtual_type; +}; + +template +struct select_overrider_virtual_type_aux< + virtual_any&&, Q, Registry> { + using type = virtual_type; +}; + +} // namespace detail + +namespace aliases { +using boost::openmethod::virtual_any; +} // namespace aliases + +} // namespace boost::openmethod + +#endif diff --git a/include/boost/openmethod/macros.hpp b/include/boost/openmethod/macros.hpp index 9fcbc84c..87371943 100644 --- a/include/boost/openmethod/macros.hpp +++ b/include/boost/openmethod/macros.hpp @@ -163,6 +163,8 @@ inline constexpr bool method_not_found = false; //! can be called with `result` and a `Registry*`, and it returns a //! `vptr_type`, call it. //! +//! @li If `virtual_traits` provides a `vptr` function, call it. +//! //! @li Call the //! [dynamic_vptr](xref:reference:boost/openmethod/policies/VptrFn/dynamic_vptr.adoc) //! of the registry's `vptr` policy. diff --git a/include/boost/openmethod/policies/vptr_map.hpp b/include/boost/openmethod/policies/vptr_map.hpp index 61927e2b..ef5d2c09 100644 --- a/include/boost/openmethod/policies/vptr_map.hpp +++ b/include/boost/openmethod/policies/vptr_map.hpp @@ -83,7 +83,7 @@ class vptr_map : public vptr { st().vptrs.swap(new_vptrs); } - //! Returns a reference to a v-table pointer for an object. + //! Returns a *reference* to a v-table pointer for an object. //! //! Acquires the dynamic @ref type_id of `arg`, using the registry's //! @ref rtti policy. @@ -96,10 +96,23 @@ class vptr_map : public vptr { //! //! @tparam Class A registered class. //! @param arg A reference to a const object of type `Class`. - //! @return A reference to a the v-table pointer for `Class`. + //! @return A reference to the v-table pointer for `Class`. template static auto dynamic_vptr(const Class& arg) -> const vptr_type& { - auto type = Registry::rtti::dynamic_type(arg); + return vptr(Registry::rtti::dynamic_type(arg)); + } + + //! Returns a *reference* to a v-table pointer for a type. + //! + //! If the registry contains the @ref runtime_checks policy, checks that + //! the map contains the type id. If it does not, and if the registry + //! contains a @ref error_handler policy, calls its + //! @ref error function with a @ref missing_class value, then + //! terminates the program with @ref abort. + //! + //! @param type A `type_id`. + //! @return A reference to the v-table pointer for `type`. + static auto vptr(type_id type) -> const vptr_type& { auto iter = st().vptrs.find(type); if constexpr (Registry::has_runtime_checks) { diff --git a/include/boost/openmethod/policies/vptr_vector.hpp b/include/boost/openmethod/policies/vptr_vector.hpp index 3c4ec999..304d2ab9 100644 --- a/include/boost/openmethod/policies/vptr_vector.hpp +++ b/include/boost/openmethod/policies/vptr_vector.hpp @@ -152,15 +152,31 @@ struct vptr_vector : vptr { //! //! @tparam Class A registered class. //! @param arg A reference to a const object of type `Class`. - //! @return A reference to a the v-table pointer for `Class`. + //! @return A reference to the v-table pointer for `Class`. template static auto dynamic_vptr(const Class& arg) -> const vptr_type& { - auto dynamic_type = Registry::rtti::dynamic_type(arg); + return vptr(Registry::rtti::dynamic_type(arg)); + }; + + //! Returns a *reference* to a v-table pointer for a type. + //! + //! If the registry has a @ref type_hash policy, uses it to convert the + //! type id to an index; otherwise, uses the type_id as the index. + //! + //! If the registry contains the @ref runtime_checks policy, verifies + //! that the index falls within the limits of the vector. If it does + //! not, and if the registry contains a @ref error_handler policy, calls + //! its @ref error function with a @ref missing_class value, then + //! terminates the program with @ref abort. + //! + //! @param type A `type_id`. + //! @return A reference to the v-table pointer for `type`. + static auto vptr(type_id type) -> const vptr_type& { std::size_t index; if constexpr (has_type_hash) { - index = type_hash::hash(dynamic_type); + index = type_hash::hash(type); } else { - index = std::size_t(dynamic_type); + index = std::size_t(type); if constexpr (Registry::has_runtime_checks) { std::size_t max_index = st().vptrs.size(); @@ -168,7 +184,7 @@ struct vptr_vector : vptr { if (index >= max_index) { if constexpr (Registry::has_error_handler) { missing_class error; - error.type = dynamic_type; + error.type = type; Registry::error_handler::error(error); } diff --git a/include/boost/openmethod/preamble.hpp b/include/boost/openmethod/preamble.hpp index 7fe9270d..0d53ee71 100644 --- a/include/boost/openmethod/preamble.hpp +++ b/include/boost/openmethod/preamble.hpp @@ -707,10 +707,29 @@ struct VptrFn { //! //! @tparam Class A registered class. //! @param arg A reference to a const object of type `Class`. - //! @return A reference to a the v-table pointer for `Class`. + //! @return A reference to the v-table pointer for `Class`. template static auto dynamic_vptr(const Class& arg) -> const vptr_type&; + // Added by the `std::any` interop, under the name `type_vptr`. An `any` + // knows the `type_id` of the value it contains, but has no object of that + // type to hand to `dynamic_vptr`. + + //! Return a *reference* to the v-table pointer for a type. + //! + //! Return a reference to the v-table pointer that `initialize` associated + //! to `type`. + //! + //! This function is optional. Implement it if the registry is to be used + //! with virtual parameters whose `virtual_traits` supply a `type_id` + //! themselves, instead of an object - see @ref VirtualTraits::vptr. Both + //! @ref vptr_vector and @ref vptr_map provide it, and implement + //! `dynamic_vptr` in terms of it. + //! + //! @param type A `type_id`. + //! @return A reference to the v-table pointer for `type`. + static auto vptr(type_id type) -> const vptr_type&; + //! Release the resources allocated by `initialize`. //! //! This function is optional. diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index f9f4524b..7fe207ca 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -155,6 +155,40 @@ openmethod_compile_fail_test( compile_fail_repeated_inheritance "repeated inheritance") openmethod_compile_fail_test( compile_fail_override_method_not_found "cannot find 'speak' method that accepts the same arguments as the overrider") +# The constrained `cast` is removed from the overload set, so the diagnostic is +# the compiler's own overload resolution failure, whose wording varies: "no +# matching function for call to" on clang and gcc, "no matching overloaded +# function found" on MSVC. +openmethod_compile_fail_test( + compile_fail_boost_any_const_ref_to_mutable_ref "no matching") +openmethod_compile_fail_test( + compile_fail_boost_any_mutable_ref_to_rvalue_ref "no matching") +openmethod_compile_fail_test( + compile_fail_virtual_any_by_value "virtual_any must be passed by reference") +# "use of a deleted function" on gcc, "call to deleted function" on clang, +# "attempting to reference a deleted function" on MSVC. +openmethod_compile_fail_test( + compile_fail_final_virtual_ptr_std_any "deleted function") +openmethod_compile_fail_test( + compile_fail_type_erasure_by_value + "an owning type_erasure::any must be passed by reference") +openmethod_compile_fail_test( + compile_fail_type_erasure_const_ref_to_mutable_ref "no matching") +openmethod_compile_fail_test( + compile_fail_final_virtual_ptr_type_erasure "deleted function") +openmethod_compile_fail_test( + compile_fail_virtual_ptr_inplace_vptr + "do not wrap an object that has a boost_openmethod_vptr overload") +openmethod_compile_fail_test( + compile_fail_virtual_ptr_virtual_any + "do not wrap a virtual_any in a virtual_ptr") +openmethod_compile_fail_test( + compile_fail_final_virtual_ptr_virtual_any + "do not wrap a virtual_any in a virtual_ptr") +openmethod_compile_fail_test( + compile_fail_std_any_custom_rtti "requires standard RTTI") +openmethod_compile_fail_test( + compile_fail_type_erasure_custom_rtti "requires standard RTTI") if (TARGET Boost::dll) add_subdirectory(dynamic_loading) diff --git a/test/Jamfile b/test/Jamfile index a1c69c4e..92001cde 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -20,6 +20,31 @@ project cxx17_structured_bindings ] /boost/openmethod//boost_openmethod + /boost/any//boost_any + + # Boost.TypeErasure, by include path rather than as a dependency. + # + # The interop needs its headers only. Its compiled part is one file, + # dynamic_binding.cpp, which nothing here references and which links + # Boost.Thread - which pulls in Boost.Container, Chrono, date_time and + # Atomic. Two of those do not build under -Werror on the Windows CI + # toolchains, and took this whole suite down with them. + # + # builds all of that. does not build it, but still is not + # enough: naming the target makes b2 load Boost.Thread's Jamfile to + # resolve type_erasure's own requirements, and that declares the + # threadapi feature, which lands in the property set of every target in + # the build - on Windows, enough to pull the dependency cluster in + # anyway. Only not naming the target at all decouples us from it. + # + # The path is relative to this Jamfile, and b2 builds openmethod inside + # boost-root, so the sibling library is where this says it is. The define + # matches type_erasure's own usage requirement: it keeps MSVC from + # auto-linking the library we are deliberately not building. Nothing here + # reaches the header that would trigger that (register_binding.hpp), but + # it costs nothing to say so. + ../../type_erasure/include + BOOST_TYPE_ERASURE_NO_LIB=1 extra diff --git a/test/compile_fail_boost_any_const_ref_to_mutable_ref.cpp b/test/compile_fail_boost_any_const_ref_to_mutable_ref.cpp new file mode 100644 index 00000000..5dce6466 --- /dev/null +++ b/test/compile_fail_boost_any_const_ref_to_mutable_ref.cpp @@ -0,0 +1,29 @@ +// Copyright (c) 2018-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 + +using namespace boost::openmethod; + +struct Dog { + std::string name; +}; + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +// The `any` is const, so boost::any_cast cannot produce a mutable reference to +// the value it contains. Without the constraint on `cast`, this would fail +// inside Boost.Any instead of at the trait. +BOOST_OPENMETHOD_OVERRIDE(name, (Dog & dog), std::string) { + return dog.name; +} + +int main() { + return 0; +} diff --git a/test/compile_fail_boost_any_mutable_ref_to_rvalue_ref.cpp b/test/compile_fail_boost_any_mutable_ref_to_rvalue_ref.cpp new file mode 100644 index 00000000..bf0db4ad --- /dev/null +++ b/test/compile_fail_boost_any_mutable_ref_to_rvalue_ref.cpp @@ -0,0 +1,39 @@ +// Copyright (c) 2018-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 + +using namespace boost::openmethod; + +struct Dog { + std::string name; +}; + +BOOST_OPENMETHOD(bump, (virtual_), std::string); + +using bump_method = + BOOST_OPENMETHOD_TYPE(bump, (virtual_), std::string); + +// Unlike std::any_cast, boost::any_cast binds an rvalue reference to the value +// stored in an lvalue `any`, which would let this overrider move the value out +// of an `any` the caller still owns. Moving the value out must go through a +// virtual_ parameter. +// +// The overrider is registered via method<...>::override because +// BOOST_OPENMETHOD_OVERRIDE cannot locate a method whose virtual parameter is +// a mutable lvalue reference to `any` - see test_dispatch_boost_any.cpp. +auto bump_dog(Dog&& dog) -> std::string { + return std::move(dog.name); +} + +BOOST_OPENMETHOD_REGISTER(bump_method::override); + +int main() { + return 0; +} diff --git a/test/compile_fail_final_virtual_ptr_std_any.cpp b/test/compile_fail_final_virtual_ptr_std_any.cpp new file mode 100644 index 00000000..6991808c --- /dev/null +++ b/test/compile_fail_final_virtual_ptr_std_any.cpp @@ -0,0 +1,25 @@ +// Copyright (c) 2018-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 + +using namespace boost::openmethod; + +struct Dog { + std::string name; +}; + +int main() { + // The primary final_virtual_ptr would use static_vptr - the + // v-table of the `any` root class, not of the contained value. The + // combination is deleted; use virtual_any instead. + std::any spot(Dog{"Spot"}); + final_virtual_ptr(spot); + return 0; +} diff --git a/test/compile_fail_final_virtual_ptr_type_erasure.cpp b/test/compile_fail_final_virtual_ptr_type_erasure.cpp new file mode 100644 index 00000000..3aeb0bc4 --- /dev/null +++ b/test/compile_fail_final_virtual_ptr_type_erasure.cpp @@ -0,0 +1,31 @@ +// Copyright (c) 2018-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 +#include + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +using Concept = boost::mpl::vector, te::relaxed>; +using erased = te::any; + +struct Dog { + std::string name; +}; + +int main() { + // The primary final_virtual_ptr would use the static v-table pointer + // of the any class itself - the root -, not the bound value's. The + // combination is deleted; use virtual_any instead. + erased spot(Dog{"Spot"}); + final_virtual_ptr(spot); + return 0; +} diff --git a/test/compile_fail_final_virtual_ptr_virtual_any.cpp b/test/compile_fail_final_virtual_ptr_virtual_any.cpp new file mode 100644 index 00000000..33786541 --- /dev/null +++ b/test/compile_fail_final_virtual_ptr_virtual_any.cpp @@ -0,0 +1,27 @@ +// Copyright (c) 2018-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 + +using namespace boost::openmethod; + +struct Dog { + std::string name; +}; + +int main() { + // final_virtual_ptr does not go through acquire_vptr: it takes + // static_vptr::virtual_type>, which for a virtual_any + // is the `any` root class - so it would silently return the root's + // v-table instead of the contained value's. It instantiates the + // virtual_ptr it returns, so the rejection catches this too. + virtual_std_any spot = Dog{"Spot"}; + final_virtual_ptr(spot); + return 0; +} diff --git a/test/compile_fail_std_any_custom_rtti.cpp b/test/compile_fail_std_any_custom_rtti.cpp new file mode 100644 index 00000000..2670432c --- /dev/null +++ b/test/compile_fail_std_any_custom_rtti.cpp @@ -0,0 +1,59 @@ +// Copyright (c) 2018-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 + +using namespace boost::openmethod; + +template +struct type_tag { + static constexpr char id = 0; +}; + +// A complete rtti policy that identifies classes by the address of a per-class +// static variable, rather than by `&typeid(T)`. Nothing else in the library +// objects to it - only the `any` interop does. +struct custom_rtti : policies::rtti { + template + struct fn : defaults { + template + static constexpr bool is_polymorphic = false; + + template + static auto static_type() -> type_id { + return &type_tag::id; + } + + template + static auto dynamic_type(const T&) -> type_id { + return &type_tag::id; + } + }; +}; + +struct custom_rtti_registry + : default_registry::with::without {}; + +struct Dog { + std::string name; +}; + +// Dispatching on a `std::any` keys on the `std::type_info` returned by +// `std::any::type()`, so the registry's rtti policy must identify classes the +// same way. This one does not: the lookup key would be meaningless, and +// `type_id` being `const void*`, the call would otherwise compile silently. +BOOST_OPENMETHOD( + name, (virtual_), std::string, custom_rtti_registry); + +int main() { + // Call the method: declaring it is not enough to instantiate it on + // every compiler, and the guard lives in `virtual_traits::vptr`. + std::any dog = Dog{"Snoopy"}; + return name(dog).size(); +} diff --git a/test/compile_fail_type_erasure_by_value.cpp b/test/compile_fail_type_erasure_by_value.cpp new file mode 100644 index 00000000..bbd25cf7 --- /dev/null +++ b/test/compile_fail_type_erasure_by_value.cpp @@ -0,0 +1,34 @@ +// Copyright (c) 2018-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 +#include + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +using Concept = boost::mpl::vector, te::relaxed>; +using erased = te::any; + +struct Dog { + std::string name; +}; + +// The owning any must be passed by reference: by value, it would copy +// the `any` - and its payload - on every call. (The any references, +// any and any, may be passed by value.) +BOOST_OPENMETHOD(name, (virtual_), std::string); + +int main() { + // Call the method: declaring it is not enough to instantiate it on + // every compiler, and the guard lives in the method's body. + erased dog = Dog{"Snoopy"}; + return name(dog).size(); +} diff --git a/test/compile_fail_type_erasure_const_ref_to_mutable_ref.cpp b/test/compile_fail_type_erasure_const_ref_to_mutable_ref.cpp new file mode 100644 index 00000000..72f44a2b --- /dev/null +++ b/test/compile_fail_type_erasure_const_ref_to_mutable_ref.cpp @@ -0,0 +1,35 @@ +// Copyright (c) 2018-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 +#include + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +using Concept = boost::mpl::vector, te::relaxed>; +using erased = te::any; + +struct Dog { + std::string name; +}; + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +// The `any` is const and owns its value, so the overrider cannot take a +// mutable reference to it; the `cast` overload is removed from the +// overload set. +BOOST_OPENMETHOD_OVERRIDE(name, (Dog & dog), std::string) { + return dog.name; +} + +int main() { + return 0; +} diff --git a/test/compile_fail_type_erasure_custom_rtti.cpp b/test/compile_fail_type_erasure_custom_rtti.cpp new file mode 100644 index 00000000..117fa58b --- /dev/null +++ b/test/compile_fail_type_erasure_custom_rtti.cpp @@ -0,0 +1,66 @@ +// Copyright (c) 2018-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 +#include + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +template +struct type_tag { + static constexpr char id = 0; +}; + +// A complete rtti policy that identifies classes by the address of a per-class +// static variable, rather than by `&typeid(T)`. Nothing else in the library +// objects to it - only the type_erasure interop does. +struct custom_rtti : policies::rtti { + template + struct fn : defaults { + template + static constexpr bool is_polymorphic = false; + + template + static auto static_type() -> type_id { + return &type_tag::id; + } + + template + static auto dynamic_type(const T&) -> type_id { + return &type_tag::id; + } + }; +}; + +struct custom_rtti_registry + : default_registry::with::without {}; + +using Concept = + boost::mpl::vector, te::typeid_<>, te::relaxed>; +using erased = te::any; + +struct Dog { + std::string name; +}; + +// Dispatching on a type_erasure::any keys on the `std::type_info` returned by +// `boost::type_erasure::typeid_of`, so the registry's rtti policy must identify +// classes the same way. This one does not: the lookup key would be meaningless, +// and `type_id` being `const void*`, the call would otherwise compile silently. +BOOST_OPENMETHOD( + name, (virtual_), std::string, custom_rtti_registry); + +int main() { + // Call the method: declaring it is not enough to instantiate it on + // every compiler, and the guard lives in `virtual_traits::vptr`. + erased dog = Dog{"Snoopy"}; + return name(dog).size(); +} diff --git a/test/compile_fail_virtual_any_by_value.cpp b/test/compile_fail_virtual_any_by_value.cpp new file mode 100644 index 00000000..ebaf562e --- /dev/null +++ b/test/compile_fail_virtual_any_by_value.cpp @@ -0,0 +1,25 @@ +// Copyright (c) 2018-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 + +using namespace boost::openmethod; + +struct Dog { + std::string name; +}; + +// A virtual_any method parameter must be a reference: passing it by value +// would copy the `any` - and its payload - on every call. +BOOST_OPENMETHOD(name, (virtual_std_any), std::string); + +int main() { + virtual_std_any dog = Dog{"Snoopy"}; + return name(dog).size(); +} diff --git a/test/compile_fail_virtual_ptr_inplace_vptr.cpp b/test/compile_fail_virtual_ptr_inplace_vptr.cpp new file mode 100644 index 00000000..f0043950 --- /dev/null +++ b/test/compile_fail_virtual_ptr_inplace_vptr.cpp @@ -0,0 +1,22 @@ +// Copyright (c) 2018-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 + +using namespace boost::openmethod; + +struct Animal : inplace_vptr_base { + virtual ~Animal() = default; +}; + +// An object with a boost_openmethod_vptr overload carries its own v-table +// pointer; wrapping it in a virtual_ptr is rejected at compile time. + +int main() { + Animal animal; + virtual_ptr p(animal); + return 0; +} diff --git a/test/compile_fail_virtual_ptr_virtual_any.cpp b/test/compile_fail_virtual_ptr_virtual_any.cpp new file mode 100644 index 00000000..9b3f1016 --- /dev/null +++ b/test/compile_fail_virtual_ptr_virtual_any.cpp @@ -0,0 +1,26 @@ +// Copyright (c) 2018-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 + +using namespace boost::openmethod; + +struct Dog { + std::string name; +}; + +int main() { + // A virtual_any already carries the v-table pointer for the value it + // contains. Wrapping it would yield a wide pointer whose v-table is the + // contained value's, but whose static type is the virtual_any - which is + // deliberately not a registered class. + virtual_std_any spot = Dog{"Spot"}; + virtual_ptr p = spot; + return 0; +} diff --git a/test/test_core.cpp b/test/test_core.cpp index 5fd126b6..cc2d9695 100644 --- a/test/test_core.cpp +++ b/test/test_core.cpp @@ -283,20 +283,16 @@ namespace TEST_NS { using test_registry = test_registry_<__COUNTER__>; -const detail::word value; - struct Animal { - friend auto boost_openmethod_vptr(const Animal&, test_registry*) { - return &value; - } + friend auto + boost_openmethod_vptr(const Animal&, test_registry*) -> vptr_type; }; static_assert(detail::has_vptr_fn); static_assert(!detail::has_vptr_fn); -BOOST_AUTO_TEST_CASE(vptr_from_function) { - initialize(); - BOOST_TEST(detail::acquire_vptr(Animal{}) == &value); -} +// The hook serves dispatch (method::vptr), not virtual_ptr: acquire_vptr +// rejects classes with a boost_openmethod_vptr overload at compile time - +// see compile_fail_virtual_ptr_inplace_vptr.cpp. } // namespace TEST_NS diff --git a/test/test_dispatch_boost_any.cpp b/test/test_dispatch_boost_any.cpp new file mode 100644 index 00000000..f4842f48 --- /dev/null +++ b/test/test_dispatch_boost_any.cpp @@ -0,0 +1,401 @@ +// Copyright (c) 2018-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 +#include +#include + +#define BOOST_TEST_MODULE dispatch_boost_any +#include + +using namespace boost::openmethod; + +#define MAKE_CLASSES() \ + struct Dog { \ + std::string name; \ + }; + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as const boost::any& (const ref) + +static_assert(detail::has_vptr< + virtual_traits, + const boost::any&>); + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { + return name; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const int& value), std::string) { + std::ostringstream os; + os << value << " the integer"; + return os.str(); +} + +// A catch-all overrider may take the `any` itself; the argument is passed +// through unchanged, instead of going through boost::any_cast, which +// would throw unless the `any` contains an `any`. + +BOOST_OPENMETHOD_OVERRIDE(name, (const boost::any& arg), std::string) { + return !arg.empty() ? "something" : "nothing"; +} + +// `double` is registered because `weigh`'s overrider names it; it has no +// `name` overrider, so the catch-all applies to it. + +BOOST_OPENMETHOD(weigh, (virtual_), double); + +BOOST_OPENMETHOD_OVERRIDE(weigh, (const double& value), double) { + return value; +} + +BOOST_AUTO_TEST_CASE(boost_any_by_const_ref) { + initialize(trace()); + + const boost::any spot(Dog{"Spot"}); + const boost::any felix(std::string{"Felix the cat"}); + const boost::any answer(42); + + BOOST_TEST(name(spot) == "Spot the dog"); + BOOST_TEST(name(felix) == "Felix the cat"); + BOOST_TEST(name(answer) == "42 the integer"); + + // `double` is registered, but has no specific overrider: the catch-all, + // registered for the `boost::any` root, applies + BOOST_TEST(name(boost::any(1.5)) == "something"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as boost::any& (mutable ref) + +static_assert( + detail::has_vptr< + virtual_traits, const boost::any&>); + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(bump, (virtual_), std::string); + +// BOOST_OPENMETHOD_OVERRIDE cannot express this. It locates the method by +// checking that the overrider's parameter types can be passed to the method's +// forwarder (see enable_forwarder and the guide function in macros.hpp), and +// `Dog&` does not convert to `boost::any&`. A temporary `boost::any` binds to +// `const boost::any&` and to `boost::any&&`, which is why the other two +// reference categories can use the macro; nothing binds to a mutable lvalue +// reference. Register directly via method<...>::override instead - the +// primitive the macro itself expands to. + +using bump_method = + BOOST_OPENMETHOD_TYPE(bump, (virtual_), std::string); + +auto bump_dog(Dog& dog) -> std::string { + dog.name += " Jr."; + return dog.name + " the dog"; +} + +auto bump_string(std::string& name) -> std::string { + name += "!"; + return name; +} + +auto bump_int(int& value) -> std::string { + ++value; + std::ostringstream os; + os << value << " the integer"; + return os.str(); +} + +BOOST_OPENMETHOD_REGISTER(bump_method::override); +BOOST_OPENMETHOD_REGISTER(bump_method::override); +BOOST_OPENMETHOD_REGISTER(bump_method::override); + +BOOST_AUTO_TEST_CASE(boost_any_by_mutable_ref) { + initialize(trace()); + + boost::any spot(Dog{"Spot"}); + boost::any felix(std::string{"Felix the cat"}); + boost::any answer(41); + + BOOST_TEST(bump(spot) == "Spot Jr. the dog"); + BOOST_TEST(boost::any_cast(spot).name == "Spot Jr."); + + BOOST_TEST(bump(felix) == "Felix the cat!"); + BOOST_TEST(boost::any_cast(felix) == "Felix the cat!"); + + BOOST_TEST(bump(answer) == "42 the integer"); + BOOST_TEST(boost::any_cast(answer) == 42); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as boost::any&& (xvalue ref) + +static_assert( + detail::has_vptr< + virtual_traits, const boost::any&>); + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(steal, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(steal, (Dog && dog), std::string) { + Dog stolen(std::move(dog)); + return stolen.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(steal, (std::string && name), std::string) { + std::string stolen(std::move(name)); + return stolen; +} + +BOOST_OPENMETHOD_OVERRIDE(steal, (int&& value), std::string) { + std::ostringstream os; + os << value << " the integer"; + return os.str(); +} + +BOOST_AUTO_TEST_CASE(boost_any_by_xvalue_ref) { + initialize(trace()); + + boost::any spot(Dog{"Spot"}); + BOOST_TEST(steal(std::move(spot)) == "Spot the dog"); + // the overrider moved the name out; the `any` still owns the Dog + BOOST_TEST(!spot.empty()); + BOOST_TEST(boost::any_cast(spot).name == ""); + + boost::any felix(std::string{"Felix the cat"}); + BOOST_TEST(steal(std::move(felix)) == "Felix the cat"); + BOOST_TEST(!felix.empty()); + BOOST_TEST(boost::any_cast(felix) == ""); + + // moving an int copies it + boost::any answer(42); + BOOST_TEST(steal(std::move(answer)) == "42 the integer"); + BOOST_TEST(boost::any_cast(answer) == 42); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// catch-all overriders + +// `float` has no overrider of the methods under test; it is registered +// because `weigh`'s overrider names it. The catch-alls apply to it. + +#define MAKE_CATCH_ALL_CLASSES() \ + struct Dog { \ + std::string name; \ + }; \ + \ + BOOST_OPENMETHOD(weigh, (virtual_), float); \ + \ + BOOST_OPENMETHOD_OVERRIDE(weigh, (const float& value), float) { \ + return value; \ + } + +MAKE_CATCH_ALL_CLASSES(); + +// An overrider may take the `any` itself. Since every registered type +// derives from it, such an overrider is a catch-all, applying to any +// contained type that has no more specific overrider. + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const boost::any&), std::string) { + return "something else"; +} + +BOOST_OPENMETHOD(bump, (virtual_), std::string); + +using bump_method = + BOOST_OPENMETHOD_TYPE(bump, (virtual_), std::string); + +auto bump_dog(Dog& dog) -> std::string { + dog.name += " Jr."; + return dog.name + " the dog"; +} + +auto bump_any(boost::any&) -> std::string { + return "something else"; +} + +BOOST_OPENMETHOD_REGISTER(bump_method::override); +BOOST_OPENMETHOD_REGISTER(bump_method::override); + +BOOST_OPENMETHOD(steal, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(steal, (Dog && dog), std::string) { + Dog stolen(std::move(dog)); + return stolen.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(steal, (boost::any&&), std::string) { + return "something else"; +} + +BOOST_AUTO_TEST_CASE(boost_any_catch_all) { + initialize(trace()); + + boost::any spot(Dog{"Spot"}); + boost::any pi(3.14f); + + BOOST_TEST(weigh(pi) == 3.14f); + + BOOST_TEST(name(spot) == "Spot the dog"); + BOOST_TEST(name(pi) == "something else"); + + BOOST_TEST(bump(spot) == "Spot Jr. the dog"); + BOOST_TEST(bump(pi) == "something else"); + + BOOST_TEST(steal(boost::any(Dog{"Fido"})) == "Fido the dog"); + BOOST_TEST(steal(std::move(pi)) == "something else"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// `any` and ordinary virtual parameters mixed in one method + +MAKE_CATCH_ALL_CLASSES(); + +struct Animal { + virtual ~Animal() { + } +}; + +struct Cat : Animal {}; + +BOOST_OPENMETHOD_CLASSES(Animal, Cat); + +BOOST_OPENMETHOD( + meet, (virtual_, virtual_ptr), + std::string); + +BOOST_OPENMETHOD_OVERRIDE( + meet, (const Dog& dog, virtual_ptr), std::string) { + return dog.name + " meets a cat"; +} + +BOOST_OPENMETHOD_OVERRIDE( + meet, (const boost::any&, virtual_ptr), std::string) { + return "someone meets an animal"; +} + +BOOST_AUTO_TEST_CASE(boost_any_mixed_with_virtual_ptr) { + initialize(trace()); + + boost::any spot(Dog{"Spot"}); + boost::any pi(3.14f); + Cat felix; + + BOOST_TEST(meet(spot, felix) == "Spot meets a cat"); + BOOST_TEST(meet(pi, felix) == "someone meets an animal"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// a type that is never named statically anywhere is not registered + +struct throw_registry + : default_registry::with< + policies::runtime_checks, policies::throw_error_handler> {}; + +struct Dog { + std::string name; +}; + +using name_method = method< + struct name_id, std::string(virtual_), throw_registry>; + +auto name_dog(const Dog& dog) -> std::string { + return dog.name + " the dog"; +} + +auto name_any(const boost::any&) -> std::string { + return "something else"; +} + +BOOST_OPENMETHOD_REGISTER(name_method::override); +BOOST_OPENMETHOD_REGISTER(name_method::override); + +BOOST_AUTO_TEST_CASE(boost_any_unregistered_type) { + initialize(); + + // `double` is named nowhere: it is not registered, and even the + // catch-all does not apply; the v-table lookup is a missing_class error + boost::any pi(3.14); + BOOST_CHECK_THROW(name_method::fn(pi), missing_class); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// a class may belong to a hierarchy and be dispatched via an `any` as well + +struct Animal { + virtual ~Animal() = default; +}; + +struct Dog : Animal { + explicit Dog(std::string name) : name(std::move(name)) { + } + + std::string name; +}; + +BOOST_OPENMETHOD_CLASSES(Animal, Dog); + +BOOST_OPENMETHOD(poke, (virtual_ptr), std::string); + +BOOST_OPENMETHOD_OVERRIDE(poke, (virtual_ptr dog), std::string) { + return dog->name + " barks"; +} + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_AUTO_TEST_CASE(boost_any_class_in_hierarchy) { + initialize(trace()); + + // `Dog` has two bases in the lattice: `Animal`, registered explicitly, + // and `boost::any`, added because `name`'s overrider names `Dog` + Dog spot("Spot"); + BOOST_TEST(poke(spot) == "Spot barks"); + + boost::any any_spot(Dog{"Spot"}); + BOOST_TEST(name(any_spot) == "Spot the dog"); +} +} // namespace BOOST_OPENMETHOD_GENSYM diff --git a/test/test_dispatch_std_any.cpp b/test/test_dispatch_std_any.cpp new file mode 100644 index 00000000..8677e8e8 --- /dev/null +++ b/test/test_dispatch_std_any.cpp @@ -0,0 +1,398 @@ +// Copyright (c) 2018-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 +#include +#include + +#define BOOST_TEST_MODULE openmethod +#include + +using namespace boost::openmethod; + +#define MAKE_CLASSES() \ + struct Dog { \ + std::string name; \ + }; + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as const std::any& (const ref) + +static_assert( + detail::has_vptr< + virtual_traits, const std::any&>); + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { + return name; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const int& value), std::string) { + std::ostringstream os; + os << value << " the integer"; + return os.str(); +} + +// A catch-all overrider may take the `any` itself; the argument is passed +// through unchanged, instead of going through std::any_cast, which would +// throw unless the `any` contains an `any`. + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::any& arg), std::string) { + return arg.has_value() ? "something" : "nothing"; +} + +// `double` is registered because `weigh`'s overrider names it; it has no +// `name` overrider, so the catch-all applies to it. + +BOOST_OPENMETHOD(weigh, (virtual_), double); + +BOOST_OPENMETHOD_OVERRIDE(weigh, (const double& value), double) { + return value; +} + +BOOST_AUTO_TEST_CASE(std_any_by_const_ref) { + initialize(trace()); + + const std::any spot(Dog{"Spot"}); + const std::any felix(std::string{"Felix the cat"}); + const std::any answer(42); + + BOOST_TEST(name(spot) == "Spot the dog"); + BOOST_TEST(name(felix) == "Felix the cat"); + BOOST_TEST(name(answer) == "42 the integer"); + + // `double` is registered, but has no specific overrider: the catch-all, + // registered for the `std::any` root, applies + BOOST_TEST(name(std::any(1.5)) == "something"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as std::any& (mutable ref) + +static_assert(detail::has_vptr< + virtual_traits, const std::any&>); + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(bump, (virtual_), std::string); + +// BOOST_OPENMETHOD_OVERRIDE cannot express this. It locates the method by +// checking that the overrider's parameter types can be passed to the method's +// forwarder (see enable_forwarder and the guide function in macros.hpp), and +// `Dog&` does not convert to `std::any&`. A temporary `std::any` binds to +// `const std::any&` and to `std::any&&`, which is why the other two reference +// categories can use the macro; nothing binds to a mutable lvalue reference. +// Register directly via method<...>::override instead - the primitive the +// macro itself expands to. + +using bump_method = + BOOST_OPENMETHOD_TYPE(bump, (virtual_), std::string); + +auto bump_dog(Dog& dog) -> std::string { + dog.name += " Jr."; + return dog.name + " the dog"; +} + +auto bump_string(std::string& name) -> std::string { + name += "!"; + return name; +} + +auto bump_int(int& value) -> std::string { + ++value; + std::ostringstream os; + os << value << " the integer"; + return os.str(); +} + +BOOST_OPENMETHOD_REGISTER(bump_method::override); +BOOST_OPENMETHOD_REGISTER(bump_method::override); +BOOST_OPENMETHOD_REGISTER(bump_method::override); + +BOOST_AUTO_TEST_CASE(std_any_by_mutable_ref) { + initialize(trace()); + + std::any spot(Dog{"Spot"}); + std::any felix(std::string{"Felix the cat"}); + std::any answer(41); + + BOOST_TEST(bump(spot) == "Spot Jr. the dog"); + BOOST_TEST(std::any_cast(spot).name == "Spot Jr."); + + BOOST_TEST(bump(felix) == "Felix the cat!"); + BOOST_TEST(std::any_cast(felix) == "Felix the cat!"); + + BOOST_TEST(bump(answer) == "42 the integer"); + BOOST_TEST(std::any_cast(answer) == 42); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as std::any&& (xvalue ref) + +static_assert(detail::has_vptr< + virtual_traits, const std::any&>); + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(steal, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(steal, (Dog && dog), std::string) { + Dog stolen(std::move(dog)); + return stolen.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(steal, (std::string && name), std::string) { + std::string stolen(std::move(name)); + return stolen; +} + +BOOST_OPENMETHOD_OVERRIDE(steal, (int&& value), std::string) { + std::ostringstream os; + os << value << " the integer"; + return os.str(); +} + +BOOST_AUTO_TEST_CASE(std_any_by_xvalue_ref) { + initialize(trace()); + + std::any spot(Dog{"Spot"}); + BOOST_TEST(steal(std::move(spot)) == "Spot the dog"); + // the overrider moved the name out; the `any` still owns the Dog + BOOST_TEST(spot.has_value()); + BOOST_TEST(std::any_cast(spot).name == ""); + + std::any felix(std::string{"Felix the cat"}); + BOOST_TEST(steal(std::move(felix)) == "Felix the cat"); + BOOST_TEST(felix.has_value()); + BOOST_TEST(std::any_cast(felix) == ""); + + // moving an int copies it + std::any answer(42); + BOOST_TEST(steal(std::move(answer)) == "42 the integer"); + BOOST_TEST(std::any_cast(answer) == 42); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// catch-all overriders + +// `float` has no overrider of the methods under test; it is registered +// because `weigh`'s overrider names it. The catch-alls apply to it. + +#define MAKE_CATCH_ALL_CLASSES() \ + struct Dog { \ + std::string name; \ + }; \ + \ + BOOST_OPENMETHOD(weigh, (virtual_), float); \ + \ + BOOST_OPENMETHOD_OVERRIDE(weigh, (const float& value), float) { \ + return value; \ + } + +MAKE_CATCH_ALL_CLASSES(); + +// An overrider may take the `any` itself. Since every registered type +// derives from it, such an overrider is a catch-all, applying to any +// contained type that has no more specific overrider. + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::any&), std::string) { + return "something else"; +} + +BOOST_OPENMETHOD(bump, (virtual_), std::string); + +using bump_method = + BOOST_OPENMETHOD_TYPE(bump, (virtual_), std::string); + +auto bump_dog(Dog& dog) -> std::string { + dog.name += " Jr."; + return dog.name + " the dog"; +} + +auto bump_any(std::any&) -> std::string { + return "something else"; +} + +BOOST_OPENMETHOD_REGISTER(bump_method::override); +BOOST_OPENMETHOD_REGISTER(bump_method::override); + +BOOST_OPENMETHOD(steal, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(steal, (Dog && dog), std::string) { + Dog stolen(std::move(dog)); + return stolen.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(steal, (std::any&&), std::string) { + return "something else"; +} + +BOOST_AUTO_TEST_CASE(std_any_catch_all) { + initialize(trace()); + + std::any spot(Dog{"Spot"}); + std::any pi(3.14f); + + BOOST_TEST(weigh(pi) == 3.14f); + + BOOST_TEST(name(spot) == "Spot the dog"); + BOOST_TEST(name(pi) == "something else"); + + BOOST_TEST(bump(spot) == "Spot Jr. the dog"); + BOOST_TEST(bump(pi) == "something else"); + + BOOST_TEST(steal(std::any(Dog{"Fido"})) == "Fido the dog"); + BOOST_TEST(steal(std::move(pi)) == "something else"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// `any` and ordinary virtual parameters mixed in one method + +MAKE_CATCH_ALL_CLASSES(); + +struct Animal { + virtual ~Animal() { + } +}; + +struct Cat : Animal {}; + +BOOST_OPENMETHOD_CLASSES(Animal, Cat); + +BOOST_OPENMETHOD( + meet, (virtual_, virtual_ptr), std::string); + +BOOST_OPENMETHOD_OVERRIDE( + meet, (const Dog& dog, virtual_ptr), std::string) { + return dog.name + " meets a cat"; +} + +BOOST_OPENMETHOD_OVERRIDE( + meet, (const std::any&, virtual_ptr), std::string) { + return "someone meets an animal"; +} + +BOOST_AUTO_TEST_CASE(std_any_mixed_with_virtual_ptr) { + initialize(trace()); + + std::any spot(Dog{"Spot"}); + std::any pi(3.14f); + Cat felix; + + BOOST_TEST(meet(spot, felix) == "Spot meets a cat"); + BOOST_TEST(meet(pi, felix) == "someone meets an animal"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// a type that is never named statically anywhere is not registered + +struct throw_registry + : default_registry::with< + policies::runtime_checks, policies::throw_error_handler> {}; + +struct Dog { + std::string name; +}; + +using name_method = method< + struct name_id, std::string(virtual_), throw_registry>; + +auto name_dog(const Dog& dog) -> std::string { + return dog.name + " the dog"; +} + +auto name_any(const std::any&) -> std::string { + return "something else"; +} + +BOOST_OPENMETHOD_REGISTER(name_method::override); +BOOST_OPENMETHOD_REGISTER(name_method::override); + +BOOST_AUTO_TEST_CASE(std_any_unregistered_type) { + initialize(); + + // `double` is named nowhere: it is not registered, and even the + // catch-all does not apply; the v-table lookup is a missing_class error + std::any pi(3.14); + BOOST_CHECK_THROW(name_method::fn(pi), missing_class); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// a class may belong to a hierarchy and be dispatched via an `any` as well + +struct Animal { + virtual ~Animal() = default; +}; + +struct Dog : Animal { + explicit Dog(std::string name) : name(std::move(name)) { + } + + std::string name; +}; + +BOOST_OPENMETHOD_CLASSES(Animal, Dog); + +BOOST_OPENMETHOD(poke, (virtual_ptr), std::string); + +BOOST_OPENMETHOD_OVERRIDE(poke, (virtual_ptr dog), std::string) { + return dog->name + " barks"; +} + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_AUTO_TEST_CASE(std_any_class_in_hierarchy) { + initialize(trace()); + + // `Dog` has two bases in the lattice: `Animal`, registered explicitly, + // and `std::any`, added because `name`'s overrider names `Dog` + Dog spot("Spot"); + BOOST_TEST(poke(spot) == "Spot barks"); + + std::any any_spot(Dog{"Spot"}); + BOOST_TEST(name(any_spot) == "Spot the dog"); +} +} // namespace BOOST_OPENMETHOD_GENSYM diff --git a/test/test_dispatch_type_erasure.cpp b/test/test_dispatch_type_erasure.cpp new file mode 100644 index 00000000..d3097ad4 --- /dev/null +++ b/test/test_dispatch_type_erasure.cpp @@ -0,0 +1,388 @@ +// Copyright (c) 2018-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 +#include + +#include +#include +#include + +#define BOOST_TEST_MODULE openmethod +#include + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +// `relaxed` implies typeid_<>, on which typeid_of and any_cast - thus +// dispatch - rely; no explicit typeid_<> needed. +using Concept = boost::mpl::vector, te::relaxed>; +using erased = te::any; +using erased_ref = te::any; +using erased_cref = te::any; + +static_assert(detail::has_vptr< + virtual_traits, const erased&>); + +#define MAKE_CLASSES() \ + struct Dog { \ + std::string name; \ + }; + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as const any& (const ref) + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { + return name; +} + +// A catch-all overrider may take the `any` itself; the argument is passed +// through unchanged. +BOOST_OPENMETHOD_OVERRIDE(name, (const erased& arg), std::string) { + return te::is_empty(arg) ? "nothing" : "something"; +} + +// `int` is registered because `weigh`'s overrider names it; it has no +// `name` overrider, so the catch-all applies to it. + +BOOST_OPENMETHOD(weigh, (virtual_), int); + +BOOST_OPENMETHOD_OVERRIDE(weigh, (const int& value), int) { + return value; +} + +BOOST_AUTO_TEST_CASE(type_erasure_by_const_ref) { + initialize(trace()); + + const erased spot(Dog{"Spot"}); + const erased felix(std::string{"Felix the cat"}); + const erased answer(42); + + BOOST_TEST(weigh(answer) == 42); + + BOOST_TEST(name(spot) == "Spot the dog"); + BOOST_TEST(name(felix) == "Felix the cat"); + // `int` is registered, but has no specific overrider: the catch-all, + // registered for the root, applies + BOOST_TEST(name(answer) == "something"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as any& (mutable ref) + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(bump, (virtual_), std::string); + +// BOOST_OPENMETHOD_OVERRIDE cannot express this: a temporary `any` binds +// to `const any&` and to `any&&`, but nothing binds to a mutable lvalue +// reference. Register directly via method<...>::override instead - +// the primitive the macro itself expands to. + +using bump_method = + BOOST_OPENMETHOD_TYPE(bump, (virtual_), std::string); + +auto bump_dog(Dog& dog) -> std::string { + dog.name += " Jr."; + return dog.name + " the dog"; +} + +auto bump_int(int& value) -> std::string { + ++value; + return "bumped"; +} + +BOOST_OPENMETHOD_REGISTER(bump_method::override); +BOOST_OPENMETHOD_REGISTER(bump_method::override); + +BOOST_AUTO_TEST_CASE(type_erasure_by_mutable_ref) { + initialize(trace()); + + erased spot(Dog{"Spot"}); + BOOST_TEST(bump(spot) == "Spot Jr. the dog"); + // the mutation is visible through the `any` + BOOST_TEST(te::any_cast(spot).name == "Spot Jr."); + + erased answer(41); + BOOST_TEST(bump(answer) == "bumped"); + BOOST_TEST(te::any_cast(answer) == 42); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as any&& (xvalue ref) + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(steal, (virtual_), std::string); + +// boost::type_erasure::any_cast has no rvalue overload; the trait moves +// the result of a mutable-reference cast, because the `any` owns its +// value. +BOOST_OPENMETHOD_OVERRIDE(steal, (Dog && dog), std::string) { + Dog stolen(std::move(dog)); + return stolen.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(steal, (std::string && name), std::string) { + std::string stolen(std::move(name)); + return stolen; +} + +BOOST_AUTO_TEST_CASE(type_erasure_by_xvalue_ref) { + initialize(trace()); + + erased spot(Dog{"Spot"}); + BOOST_TEST(steal(std::move(spot)) == "Spot the dog"); + // the overrider moved the name out; the `any` still owns the Dog + BOOST_TEST(!te::is_empty(spot)); + BOOST_TEST(te::any_cast(spot).name == ""); + + erased felix(std::string{"Felix the cat"}); + BOOST_TEST(steal(std::move(felix)) == "Felix the cat"); + BOOST_TEST(te::any_cast(felix) == ""); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as any - the mutable any reference - by +// value + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(poke, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(poke, (Dog & dog), std::string) { + dog.name += "!"; + return dog.name; +} + +BOOST_OPENMETHOD_OVERRIDE(poke, (int& value), std::string) { + ++value; + return "poked"; +} + +BOOST_AUTO_TEST_CASE(type_erasure_any_ref_by_value) { + initialize(trace()); + + // the any reference is a cheap handle; mutations reach the referents + Dog snoopy{"Snoopy"}; + int count = 41; + + BOOST_TEST(poke(erased_ref(snoopy)) == "Snoopy!"); + BOOST_TEST(snoopy.name == "Snoopy!"); + + BOOST_TEST(poke(erased_ref(count)) == "poked"); + BOOST_TEST(count == 42); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as any - the const any reference - +// by value + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +// the catch-all receives a copy of the any reference - still a cheap handle +BOOST_OPENMETHOD_OVERRIDE(name, (erased_cref arg), std::string) { + return te::is_empty(arg) ? "nothing" : "something"; +} + +// `int` is registered because `weigh`'s overrider names it; it has no +// `name` overrider, so the catch-all applies to it. + +BOOST_OPENMETHOD(weigh, (virtual_), int); + +BOOST_OPENMETHOD_OVERRIDE(weigh, (const int& value), int) { + return value; +} + +BOOST_AUTO_TEST_CASE(type_erasure_const_any_ref_by_value) { + initialize(trace()); + + Dog snoopy{"Snoopy"}; + const int count = 42; + + BOOST_TEST(weigh(erased_cref(count)) == 42); + + BOOST_TEST(name(erased_cref(snoopy)) == "Snoopy the dog"); + BOOST_TEST(name(erased_cref(count)) == "something"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// indirect vptrs + +struct Dog { + std::string name; +}; + +// `Dog` is registered in `indirect_registry` - the method's registry - by +// the overrider + +using name_method = method< + struct name_id, std::string(virtual_), indirect_registry>; + +auto name_dog(const Dog& dog) -> std::string { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_REGISTER(name_method::override); + +BOOST_AUTO_TEST_CASE(type_erasure_indirect_vptr) { + initialize(); + + const erased spot(Dog{"Spot"}); + BOOST_TEST(name_method::fn(spot) == "Spot the dog"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// the openmethod_vptr concept: the any carries the v-table pointer for its +// bound type in its own dispatch table, and binding a value registers its type + +struct Dog { + std::string name; +}; + +// The concept must name the Concept it is part of: define the Concept as +// a struct. +struct Dispatchable : boost::mpl::vector< + te::copy_constructible<>, te::relaxed, + openmethod_vptr> {}; + +using dispatchable = te::any; +using dispatchable_ref = te::any; + +// the intrinsic hook is found for every any, so dispatch prefers it +// over the vptr policy's hash lookup +static_assert(detail::has_vptr_fn); +static_assert(detail::has_vptr_fn); + +// `Dog` is registered both by the `name` overrider below and by binding +// (class dedup) + +BOOST_OPENMETHOD(name, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const dispatchable& value), std::string) { + return te::is_empty(value) ? "nothing" : "something"; +} + +BOOST_OPENMETHOD(poke, (virtual_), std::string); + +BOOST_OPENMETHOD_OVERRIDE(poke, (Dog & dog), std::string) { + dog.name += "!"; + return dog.name; +} + +BOOST_AUTO_TEST_CASE(type_erasure_openmethod_vptr_concept) { + initialize(trace()); + + const dispatchable spot(Dog{"Spot"}); + + // the hook returns the static vptr for the bound type + BOOST_TEST( + boost_openmethod_vptr(spot, static_cast(nullptr)) == + default_registry::static_vptr); + BOOST_TEST(name(spot) == "Spot the dog"); + + // std::string appears nowhere in this section; binding it registered + // it, and the catch-all applies + const dispatchable felix(std::string{"Felix"}); + BOOST_TEST(name(felix) == "something"); + + // the any reference takes the fast path too + Dog snoopy{"Snoopy"}; + BOOST_TEST(poke(dispatchable_ref(snoopy)) == "Snoopy!"); + BOOST_TEST(snoopy.name == "Snoopy!"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// the openmethod_vptr concept, with indirect vptrs + +struct Dog { + std::string name; +}; + +struct Dispatchable : boost::mpl::vector< + te::copy_constructible<>, te::relaxed, + openmethod_vptr> {}; + +using dispatchable = te::any; + +// The hook is keyed on the registry the concept names: it is found for +// `indirect_registry`, and for no other - a mismatch would fall back on the +// vptr policy's hash lookup, silently losing the constant-time property. +static_assert(detail::has_vptr_fn); +static_assert(!detail::has_vptr_fn); + +using name_method = method< + struct name_id, std::string(virtual_), + indirect_registry>; + +auto name_dog(const Dog& dog) -> std::string { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_REGISTER(name_method::override); + +BOOST_AUTO_TEST_CASE(type_erasure_openmethod_vptr_indirect) { + initialize(); + + const dispatchable spot(Dog{"Spot"}); + BOOST_TEST(name_method::fn(spot) == "Spot the dog"); + + // The concept needs no indirection of its own - unlike inplace_vptr, + // which stores a `const vptr_type*` under this policy. `apply` reads + // `static_vptr` on every call, so it tracks a re-initialize, which + // rebuilds the v-tables and updates `static_vptr`. + initialize(); + + BOOST_TEST( + boost_openmethod_vptr(spot, static_cast(nullptr)) == + indirect_registry::static_vptr); + BOOST_TEST(name_method::fn(spot) == "Spot the dog"); +} +} // namespace BOOST_OPENMETHOD_GENSYM diff --git a/test/test_type_erasure_static_rtti.cpp b/test/test_type_erasure_static_rtti.cpp new file mode 100644 index 00000000..fea380f0 --- /dev/null +++ b/test/test_type_erasure_static_rtti.cpp @@ -0,0 +1,66 @@ +// Copyright (c) 2018-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 + +#include +#include +#include + +#define BOOST_TEST_MODULE openmethod +#include + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +// The `openmethod_vptr` concept takes the v-table pointer from the `any`'s own +// dispatch table, so it does not go through `typeid_of`, and the `std_rtti` +// requirement that the `typeid_of`-based traits assert does not apply. The +// registry then needs an rtti policy only for the static type identification +// `initialize()` performs - `static_rtti` suffices - and neither a `vptr` +// policy nor the `type_hash` one would depend on. +struct minimal_registry : registry {}; + +struct Dog { + std::string name; +}; + +struct Cat { + std::string name; +}; + +struct Dispatchable : boost::mpl::vector< + te::copy_constructible<>, te::relaxed, + openmethod_vptr> {}; + +using erased = te::any; + +// Binding a value to the `any` registers its type. + +BOOST_OPENMETHOD( + name, (virtual_), std::string, minimal_registry); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const Cat& cat), std::string) { + return cat.name + " the cat"; +} + +BOOST_AUTO_TEST_CASE(type_erasure_openmethod_vptr_needs_no_std_rtti) { + initialize(); + + const erased spot(Dog{"Spot"}); + const erased tom(Cat{"Tom"}); + + BOOST_TEST(name(spot) == "Spot the dog"); + BOOST_TEST(name(tom) == "Tom the cat"); +} diff --git a/test/test_virtual_any_boost.cpp b/test/test_virtual_any_boost.cpp new file mode 100644 index 00000000..a1f78883 --- /dev/null +++ b/test/test_virtual_any_boost.cpp @@ -0,0 +1,336 @@ +// Copyright (c) 2018-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 +#include +#include +#include + +#define BOOST_TEST_MODULE openmethod +#include + +using namespace boost::openmethod; + +#define MAKE_CLASSES() \ + struct Dog { \ + std::string name; \ + }; + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as const virtual_boost_any& (const ref) + +// A virtual_any hands out the v-table pointer it cached, through its +// virtual_traits, for all three reference categories. +static_assert(detail::has_vptr< + virtual_traits, + const virtual_boost_any&>); +static_assert(detail::has_vptr< + virtual_traits, + const virtual_boost_any&>); +static_assert(detail::has_vptr< + virtual_traits, + const virtual_boost_any&>); + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (const virtual_boost_any&), std::string); + +// The overriders can use the macro: the value constructor of virtual_any +// makes the overrider's parameter convertible to the method's, so the +// method is located. + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { + return name; +} + +// A catch-all overrider may keep the wrapper. +BOOST_OPENMETHOD_OVERRIDE(name, (const virtual_boost_any& va), std::string) { + return !va.get().empty() ? "something" : "nothing"; +} + +BOOST_AUTO_TEST_CASE(virtual_any_by_const_ref) { + initialize(trace()); + + // from an `any`: the v-table pointer is looked up from the dynamic + // type of the contained value + const boost::any spot_any(Dog{"Spot"}); + virtual_boost_any spot = spot_any; + BOOST_TEST(spot.vptr() == default_registry::static_vptr); + BOOST_TEST(name(spot) == "Spot the dog"); + + // from a value: the v-table pointer is set statically + virtual_boost_any rex = Dog{"Rex"}; + BOOST_TEST(rex.vptr() == default_registry::static_vptr); + BOOST_TEST(name(rex) == "Rex the dog"); + + virtual_boost_any felix = std::string("Felix the cat"); + BOOST_TEST(felix.vptr() == default_registry::static_vptr); + BOOST_TEST(name(felix) == "Felix the cat"); + + // a value converts to a (temporary) virtual_any at the call site + BOOST_TEST(name(Dog{"Fido"}) == "Fido the dog"); + + // `int` is registered automatically - the value conversion at the call + // site stores it - but has no specific overrider: the catch-all, + // registered for the `boost::any` root, applies + BOOST_TEST(name(42) == "something"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as virtual_boost_any& (mutable ref) + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(bump, (virtual_boost_any&), std::string); + +// BOOST_OPENMETHOD_OVERRIDE cannot express this: a temporary virtual_any +// binds to `const virtual_boost_any&` and to `virtual_boost_any&&`, but +// nothing binds to a mutable lvalue reference. Register directly via +// method<...>::override instead - the primitive the macro itself +// expands to. + +using bump_method = + BOOST_OPENMETHOD_TYPE(bump, (virtual_boost_any&), std::string); + +auto bump_dog(Dog& dog) -> std::string { + dog.name += " Jr."; + return dog.name + " the dog"; +} + +auto bump_int(int& value) -> std::string { + ++value; + return "bumped"; +} + +BOOST_OPENMETHOD_REGISTER(bump_method::override); +BOOST_OPENMETHOD_REGISTER(bump_method::override); + +BOOST_AUTO_TEST_CASE(virtual_any_by_mutable_ref) { + initialize(trace()); + + virtual_boost_any spot = Dog{"Spot"}; + BOOST_TEST(bump(spot) == "Spot Jr. the dog"); + // the mutation is visible through the virtual_any + BOOST_TEST(boost::any_cast(spot.get()).name == "Spot Jr."); + + virtual_boost_any answer = 41; + BOOST_TEST(bump(answer) == "bumped"); + BOOST_TEST(boost::any_cast(answer.get()) == 42); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as virtual_boost_any&& (xvalue ref) + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(steal, (virtual_boost_any&&), std::string); + +BOOST_OPENMETHOD_OVERRIDE(steal, (Dog && dog), std::string) { + Dog stolen(std::move(dog)); + return stolen.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(steal, (std::string && name), std::string) { + std::string stolen(std::move(name)); + return stolen; +} + +BOOST_AUTO_TEST_CASE(virtual_any_by_xvalue_ref) { + initialize(trace()); + + virtual_boost_any spot = Dog{"Spot"}; + BOOST_TEST(steal(std::move(spot)) == "Spot the dog"); + // the overrider moved the name out; the virtual_any still owns the Dog + BOOST_TEST(!spot.get().empty()); + BOOST_TEST(boost::any_cast(spot.get()).name == ""); + + BOOST_TEST( + steal(virtual_boost_any(std::string("Felix the cat"))) == + "Felix the cat"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// value semantics + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (const virtual_boost_any&), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_AUTO_TEST_CASE(virtual_any_value_semantics) { + initialize(trace()); + + virtual_boost_any empty; + BOOST_TEST(empty.get().empty()); + BOOST_TEST(empty.vptr() == nullptr); + + virtual_boost_any rex = Dog{"Rex"}; + + // copy: independent payloads, same v-table pointer + auto copy = rex; + BOOST_TEST(copy.vptr() == rex.vptr()); + BOOST_TEST(name(copy) == "Rex the dog"); + BOOST_TEST(name(rex) == "Rex the dog"); // original unaffected + + // move: the source's v-table pointer is nulled + auto moved = std::move(copy); + BOOST_TEST(moved.vptr() == default_registry::static_vptr); + BOOST_TEST(copy.vptr() == nullptr); + BOOST_TEST(name(moved) == "Rex the dog"); + + // assignment from an `any` re-derives the v-table pointer; + // `std::string` is registered by the `emplace` below + boost::any felix_any(std::string{"Felix"}); + moved = felix_any; + BOOST_TEST(moved.vptr() == default_registry::static_vptr); + + // assignment from a value sets it statically + moved = Dog{"Snoopy"}; + BOOST_TEST(moved.vptr() == default_registry::static_vptr); + BOOST_TEST(name(moved) == "Snoopy the dog"); + + // emplace constructs in place and sets it statically + moved.emplace("Sylvester"); + BOOST_TEST(moved.vptr() == default_registry::static_vptr); + BOOST_TEST(boost::any_cast(moved.get()) == "Sylvester"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// indirect vptrs + +struct Dog { + std::string name; +}; + +// `Dog` is registered in `indirect_registry` - the method's registry - by +// the overrider and by the value constructor + +using name_method = method< + struct name_id, + std::string(const virtual_any&), + indirect_registry>; + +auto name_dog(const Dog& dog) -> std::string { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_REGISTER(name_method::override); + +BOOST_AUTO_TEST_CASE(virtual_any_indirect_vptr) { + initialize(); + + boost::any spot_any(Dog{"Spot"}); + virtual_any spot = spot_any; + BOOST_TEST(spot.vptr() == indirect_registry::static_vptr); + BOOST_TEST(name_method::fn(spot) == "Spot the dog"); + + virtual_any rex = Dog{"Rex"}; + BOOST_TEST(name_method::fn(rex) == "Rex the dog"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// a type that is never named statically anywhere is not registered + +struct throw_registry + : default_registry::with< + policies::runtime_checks, policies::throw_error_handler> {}; + +struct Dog { + std::string name; +}; + +using throw_any = virtual_any; + +using name_method = + method; + +auto name_dog(const Dog& dog) -> std::string { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_REGISTER(name_method::override); + +BOOST_AUTO_TEST_CASE(virtual_any_unregistered_type) { + initialize(); + + // `double` is named nowhere: it is not registered, and constructing or + // assigning a virtual_any from an `any` containing one is a + // missing_class error at the construction/assignment site + boost::any pi(3.14); + BOOST_CHECK_THROW(throw_any{pi}, missing_class); + + throw_any va; + BOOST_CHECK_THROW(va = pi, missing_class); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// mixing with ordinary virtual parameters + +MAKE_CLASSES(); + +struct Animal { + virtual ~Animal() { + } +}; + +struct Cat : Animal {}; + +BOOST_OPENMETHOD_CLASSES(Animal, Cat); + +BOOST_OPENMETHOD( + meet, (const virtual_boost_any&, virtual_ptr), std::string); + +BOOST_OPENMETHOD_OVERRIDE( + meet, (const Dog& dog, virtual_ptr), std::string) { + return dog.name + " meets a cat"; +} + +BOOST_OPENMETHOD_OVERRIDE( + meet, (const virtual_boost_any&, virtual_ptr), std::string) { + return "someone meets an animal"; +} + +BOOST_AUTO_TEST_CASE(virtual_any_mixed_with_virtual_ptr) { + initialize(trace()); + + virtual_boost_any spot = Dog{"Spot"}; + virtual_boost_any pi = 3.14f; + Cat felix; + + BOOST_TEST(meet(spot, felix) == "Spot meets a cat"); + BOOST_TEST(meet(pi, felix) == "someone meets an animal"); +} +} // namespace BOOST_OPENMETHOD_GENSYM diff --git a/test/test_virtual_any_std.cpp b/test/test_virtual_any_std.cpp new file mode 100644 index 00000000..c6caad19 --- /dev/null +++ b/test/test_virtual_any_std.cpp @@ -0,0 +1,336 @@ +// Copyright (c) 2018-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 +#include +#include +#include + +#define BOOST_TEST_MODULE openmethod +#include + +using namespace boost::openmethod; + +#define MAKE_CLASSES() \ + struct Dog { \ + std::string name; \ + }; + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as const virtual_std_any& (const ref) + +// A virtual_any hands out the v-table pointer it cached, through its +// virtual_traits, for all three reference categories. +static_assert(detail::has_vptr< + virtual_traits, + const virtual_std_any&>); +static_assert(detail::has_vptr< + virtual_traits, + const virtual_std_any&>); +static_assert(detail::has_vptr< + virtual_traits, + const virtual_std_any&>); + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (const virtual_std_any&), std::string); + +// The overriders can use the macro: the value constructor of virtual_any +// makes the overrider's parameter convertible to the method's, so the +// method is located. + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { + return name; +} + +// A catch-all overrider may keep the wrapper. +BOOST_OPENMETHOD_OVERRIDE(name, (const virtual_std_any& va), std::string) { + return va.get().has_value() ? "something" : "nothing"; +} + +BOOST_AUTO_TEST_CASE(virtual_any_by_const_ref) { + initialize(trace()); + + // from an `any`: the v-table pointer is looked up from the dynamic + // type of the contained value + const std::any spot_any(Dog{"Spot"}); + virtual_std_any spot = spot_any; + BOOST_TEST(spot.vptr() == default_registry::static_vptr); + BOOST_TEST(name(spot) == "Spot the dog"); + + // from a value: the v-table pointer is set statically + virtual_std_any rex = Dog{"Rex"}; + BOOST_TEST(rex.vptr() == default_registry::static_vptr); + BOOST_TEST(name(rex) == "Rex the dog"); + + virtual_std_any felix = std::string("Felix the cat"); + BOOST_TEST(felix.vptr() == default_registry::static_vptr); + BOOST_TEST(name(felix) == "Felix the cat"); + + // a value converts to a (temporary) virtual_any at the call site + BOOST_TEST(name(Dog{"Fido"}) == "Fido the dog"); + + // `int` is registered automatically - the value conversion at the call + // site stores it - but has no specific overrider: the catch-all, + // registered for the `std::any` root, applies + BOOST_TEST(name(42) == "something"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as virtual_std_any& (mutable ref) + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(bump, (virtual_std_any&), std::string); + +// BOOST_OPENMETHOD_OVERRIDE cannot express this: a temporary virtual_any +// binds to `const virtual_std_any&` and to `virtual_std_any&&`, but +// nothing binds to a mutable lvalue reference. Register directly via +// method<...>::override instead - the primitive the macro itself +// expands to. + +using bump_method = + BOOST_OPENMETHOD_TYPE(bump, (virtual_std_any&), std::string); + +auto bump_dog(Dog& dog) -> std::string { + dog.name += " Jr."; + return dog.name + " the dog"; +} + +auto bump_int(int& value) -> std::string { + ++value; + return "bumped"; +} + +BOOST_OPENMETHOD_REGISTER(bump_method::override); +BOOST_OPENMETHOD_REGISTER(bump_method::override); + +BOOST_AUTO_TEST_CASE(virtual_any_by_mutable_ref) { + initialize(trace()); + + virtual_std_any spot = Dog{"Spot"}; + BOOST_TEST(bump(spot) == "Spot Jr. the dog"); + // the mutation is visible through the virtual_any + BOOST_TEST(std::any_cast(spot.get()).name == "Spot Jr."); + + virtual_std_any answer = 41; + BOOST_TEST(bump(answer) == "bumped"); + BOOST_TEST(std::any_cast(answer.get()) == 42); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as virtual_std_any&& (xvalue ref) + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(steal, (virtual_std_any&&), std::string); + +BOOST_OPENMETHOD_OVERRIDE(steal, (Dog && dog), std::string) { + Dog stolen(std::move(dog)); + return stolen.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(steal, (std::string && name), std::string) { + std::string stolen(std::move(name)); + return stolen; +} + +BOOST_AUTO_TEST_CASE(virtual_any_by_xvalue_ref) { + initialize(trace()); + + virtual_std_any spot = Dog{"Spot"}; + BOOST_TEST(steal(std::move(spot)) == "Spot the dog"); + // the overrider moved the name out; the virtual_any still owns the Dog + BOOST_TEST(spot.get().has_value()); + BOOST_TEST(std::any_cast(spot.get()).name == ""); + + BOOST_TEST( + steal(virtual_std_any(std::string("Felix the cat"))) == + "Felix the cat"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// value semantics + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (const virtual_std_any&), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_AUTO_TEST_CASE(virtual_any_value_semantics) { + initialize(trace()); + + virtual_std_any empty; + BOOST_TEST(!empty.get().has_value()); + BOOST_TEST(empty.vptr() == nullptr); + + virtual_std_any rex = Dog{"Rex"}; + + // copy: independent payloads, same v-table pointer + auto copy = rex; + BOOST_TEST(copy.vptr() == rex.vptr()); + BOOST_TEST(name(copy) == "Rex the dog"); + BOOST_TEST(name(rex) == "Rex the dog"); // original unaffected + + // move: the source's v-table pointer is nulled + auto moved = std::move(copy); + BOOST_TEST(moved.vptr() == default_registry::static_vptr); + BOOST_TEST(copy.vptr() == nullptr); + BOOST_TEST(name(moved) == "Rex the dog"); + + // assignment from an `any` re-derives the v-table pointer; + // `std::string` is registered by the `emplace` below + std::any felix_any(std::string{"Felix"}); + moved = felix_any; + BOOST_TEST(moved.vptr() == default_registry::static_vptr); + + // assignment from a value sets it statically + moved = Dog{"Snoopy"}; + BOOST_TEST(moved.vptr() == default_registry::static_vptr); + BOOST_TEST(name(moved) == "Snoopy the dog"); + + // emplace constructs in place and sets it statically + moved.emplace("Sylvester"); + BOOST_TEST(moved.vptr() == default_registry::static_vptr); + BOOST_TEST(std::any_cast(moved.get()) == "Sylvester"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// indirect vptrs + +struct Dog { + std::string name; +}; + +// `Dog` is registered in `indirect_registry` - the method's registry - by +// the overrider and by the value constructor + +using name_method = method< + struct name_id, + std::string(const virtual_any&), + indirect_registry>; + +auto name_dog(const Dog& dog) -> std::string { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_REGISTER(name_method::override); + +BOOST_AUTO_TEST_CASE(virtual_any_indirect_vptr) { + initialize(); + + std::any spot_any(Dog{"Spot"}); + virtual_any spot = spot_any; + BOOST_TEST(spot.vptr() == indirect_registry::static_vptr); + BOOST_TEST(name_method::fn(spot) == "Spot the dog"); + + virtual_any rex = Dog{"Rex"}; + BOOST_TEST(name_method::fn(rex) == "Rex the dog"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// a type that is never named statically anywhere is not registered + +struct throw_registry + : default_registry::with< + policies::runtime_checks, policies::throw_error_handler> {}; + +struct Dog { + std::string name; +}; + +using throw_any = virtual_any; + +using name_method = + method; + +auto name_dog(const Dog& dog) -> std::string { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_REGISTER(name_method::override); + +BOOST_AUTO_TEST_CASE(virtual_any_unregistered_type) { + initialize(); + + // `double` is named nowhere: it is not registered, and constructing or + // assigning a virtual_any from an `any` containing one is a + // missing_class error at the construction/assignment site + std::any pi(3.14); + BOOST_CHECK_THROW(throw_any{pi}, missing_class); + + throw_any va; + BOOST_CHECK_THROW(va = pi, missing_class); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// mixing with ordinary virtual parameters + +MAKE_CLASSES(); + +struct Animal { + virtual ~Animal() { + } +}; + +struct Cat : Animal {}; + +BOOST_OPENMETHOD_CLASSES(Animal, Cat); + +BOOST_OPENMETHOD( + meet, (const virtual_std_any&, virtual_ptr), std::string); + +BOOST_OPENMETHOD_OVERRIDE( + meet, (const Dog& dog, virtual_ptr), std::string) { + return dog.name + " meets a cat"; +} + +BOOST_OPENMETHOD_OVERRIDE( + meet, (const virtual_std_any&, virtual_ptr), std::string) { + return "someone meets an animal"; +} + +BOOST_AUTO_TEST_CASE(virtual_any_mixed_with_virtual_ptr) { + initialize(trace()); + + virtual_std_any spot = Dog{"Spot"}; + virtual_std_any pi = 3.14f; + Cat felix; + + BOOST_TEST(meet(spot, felix) == "Spot meets a cat"); + BOOST_TEST(meet(pi, felix) == "someone meets an animal"); +} +} // namespace BOOST_OPENMETHOD_GENSYM diff --git a/test/test_virtual_any_type_erasure.cpp b/test/test_virtual_any_type_erasure.cpp new file mode 100644 index 00000000..b382ffa8 --- /dev/null +++ b/test/test_virtual_any_type_erasure.cpp @@ -0,0 +1,256 @@ +// Copyright (c) 2018-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 +#include +#include + +#include +#include +#include + +#define BOOST_TEST_MODULE openmethod +#include + +namespace te = boost::type_erasure; +using namespace boost::openmethod; + +// `relaxed` is needed for virtual_any's default constructor and +// assignment, `copy_constructible<>` for its copies. `relaxed` also +// implies typeid_<>, on which the v-table lookup relies. +using Concept = boost::mpl::vector, te::relaxed>; +using erased = te::any; +using virtual_erased = virtual_any; + +#define MAKE_CLASSES() \ + struct Dog { \ + std::string name; \ + }; + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as const virtual_any& (const ref) + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (const virtual_erased&), std::string); + +// The overriders can use the macro: the value constructor of virtual_any +// makes the overrider's parameter convertible to the method's, so the +// method is located. + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) { + return name; +} + +// A catch-all overrider may keep the wrapper. +BOOST_OPENMETHOD_OVERRIDE(name, (const virtual_erased& va), std::string) { + return !te::is_empty(va.get()) ? "something" : "nothing"; +} + +BOOST_AUTO_TEST_CASE(virtual_any_by_const_ref) { + initialize(trace()); + + // from an `any`: the v-table pointer is looked up from the type bound + // to the `any` + const erased spot_any(Dog{"Spot"}); + virtual_erased spot = spot_any; + BOOST_TEST(spot.vptr() == default_registry::static_vptr); + BOOST_TEST(name(spot) == "Spot the dog"); + + // from a value: the v-table pointer is set statically + virtual_erased rex = Dog{"Rex"}; + BOOST_TEST(rex.vptr() == default_registry::static_vptr); + BOOST_TEST(name(rex) == "Rex the dog"); + + virtual_erased felix = std::string("Felix the cat"); + BOOST_TEST(felix.vptr() == default_registry::static_vptr); + BOOST_TEST(name(felix) == "Felix the cat"); + + // a value converts to a (temporary) virtual_any at the call site + BOOST_TEST(name(Dog{"Fido"}) == "Fido the dog"); + + // `int` is registered automatically - the value conversion at the call + // site stores it - but has no specific overrider: the catch-all, + // registered for the owning any, applies + BOOST_TEST(name(42) == "something"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as virtual_any& (mutable ref) + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(bump, (virtual_erased&), std::string); + +// BOOST_OPENMETHOD_OVERRIDE cannot express this: a temporary virtual_any +// binds to `const virtual_erased&` and to `virtual_erased&&`, but nothing +// binds to a mutable lvalue reference. Register directly via +// method<...>::override instead - the primitive the macro itself +// expands to. + +using bump_method = BOOST_OPENMETHOD_TYPE(bump, (virtual_erased&), std::string); + +auto bump_dog(Dog& dog) -> std::string { + dog.name += " Jr."; + return dog.name + " the dog"; +} + +auto bump_int(int& value) -> std::string { + ++value; + return "bumped"; +} + +BOOST_OPENMETHOD_REGISTER(bump_method::override); +BOOST_OPENMETHOD_REGISTER(bump_method::override); + +BOOST_AUTO_TEST_CASE(virtual_any_by_mutable_ref) { + initialize(trace()); + + virtual_erased spot = Dog{"Spot"}; + BOOST_TEST(bump(spot) == "Spot Jr. the dog"); + // the mutation is visible through the virtual_any + BOOST_TEST(te::any_cast(spot.get()).name == "Spot Jr."); + + virtual_erased answer = 41; + BOOST_TEST(bump(answer) == "bumped"); + BOOST_TEST(te::any_cast(answer.get()) == 42); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// pass virtual args as virtual_any&& (xvalue ref) + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(steal, (virtual_erased&&), std::string); + +// boost::type_erasure::any_cast has no rvalue overload; the trait moves +// the result of a mutable-reference cast, because the `any` owns its +// value. +BOOST_OPENMETHOD_OVERRIDE(steal, (Dog && dog), std::string) { + Dog stolen(std::move(dog)); + return stolen.name + " the dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(steal, (std::string && name), std::string) { + std::string stolen(std::move(name)); + return stolen; +} + +BOOST_AUTO_TEST_CASE(virtual_any_by_xvalue_ref) { + initialize(trace()); + + virtual_erased spot = Dog{"Spot"}; + BOOST_TEST(steal(std::move(spot)) == "Spot the dog"); + // the overrider moved the name out; the virtual_any still owns the Dog + BOOST_TEST(!te::is_empty(spot.get())); + BOOST_TEST(te::any_cast(spot.get()).name == ""); + + BOOST_TEST( + steal(virtual_erased(std::string("Felix the cat"))) == "Felix the cat"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// value semantics + +MAKE_CLASSES(); + +BOOST_OPENMETHOD(name, (const virtual_erased&), std::string); + +BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) { + return dog.name + " the dog"; +} + +BOOST_AUTO_TEST_CASE(virtual_any_value_semantics) { + initialize(trace()); + + virtual_erased empty; + BOOST_TEST(te::is_empty(empty.get())); + BOOST_TEST(empty.vptr() == nullptr); + + virtual_erased rex = Dog{"Rex"}; + + // copy: independent payloads, same v-table pointer + auto copy = rex; + BOOST_TEST(copy.vptr() == rex.vptr()); + BOOST_TEST(name(copy) == "Rex the dog"); + BOOST_TEST(name(rex) == "Rex the dog"); // original unaffected + + // move: the source's v-table pointer is nulled + auto moved = std::move(copy); + BOOST_TEST(moved.vptr() == default_registry::static_vptr); + BOOST_TEST(copy.vptr() == nullptr); + BOOST_TEST(name(moved) == "Rex the dog"); + + // assignment from an `any` re-derives the v-table pointer; + // `std::string` is registered by the `emplace` below + erased felix_any(std::string{"Felix"}); + moved = felix_any; + BOOST_TEST(moved.vptr() == default_registry::static_vptr); + + // assignment from a value sets it statically + moved = Dog{"Snoopy"}; + BOOST_TEST(moved.vptr() == default_registry::static_vptr); + BOOST_TEST(name(moved) == "Snoopy the dog"); + + // emplace constructs in place and sets it statically + moved.emplace("Sylvester"); + BOOST_TEST(moved.vptr() == default_registry::static_vptr); + BOOST_TEST(te::any_cast(moved.get()) == "Sylvester"); +} +} // namespace BOOST_OPENMETHOD_GENSYM + +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// indirect vptrs + +struct Dog { + std::string name; +}; + +// `Dog` is registered in `indirect_registry` - the method's registry - by +// the overrider and by the value constructor + +using name_method = method< + struct name_id, std::string(const virtual_any&), + indirect_registry>; + +auto name_dog(const Dog& dog) -> std::string { + return dog.name + " the dog"; +} + +BOOST_OPENMETHOD_REGISTER(name_method::override); + +BOOST_AUTO_TEST_CASE(virtual_any_indirect_vptr) { + initialize(); + + erased spot_any(Dog{"Spot"}); + virtual_any spot = spot_any; + BOOST_TEST(spot.vptr() == indirect_registry::static_vptr); + BOOST_TEST(name_method::fn(spot) == "Spot the dog"); + + virtual_any rex = Dog{"Rex"}; + BOOST_TEST(name_method::fn(rex) == "Rex the dog"); +} +} // namespace BOOST_OPENMETHOD_GENSYM