diff --git a/doc/modules/ROOT/examples/custom_rtti/1/custom_rtti.cpp b/doc/modules/ROOT/examples/custom_rtti/1/custom_rtti.cpp index 163efd00..f08f7a4c 100644 --- a/doc/modules/ROOT/examples/custom_rtti/1/custom_rtti.cpp +++ b/doc/modules/ROOT/examples/custom_rtti/1/custom_rtti.cpp @@ -5,6 +5,9 @@ // clang-format off +#include +#include + // tag::classes[] struct Node { virtual ~Node() {} @@ -56,12 +59,22 @@ struct custom_rtti : boost::openmethod::policies::rtti { using type_id = boost::openmethod::type_id; // for brevity + template + inline static std::uintptr_t np_id = 0; + + inline static std::uintptr_t np_id_alloc = + std::numeric_limits::max(); + template static auto static_type() { if constexpr (is_polymorphic) { return reinterpret_cast(T::static_type); } else { - return reinterpret_cast(0); + if (np_id == 0) { + np_id = np_id_alloc--; + } + + return reinterpret_cast(np_id); } } diff --git a/doc/modules/ROOT/examples/custom_rtti/2/custom_rtti.cpp b/doc/modules/ROOT/examples/custom_rtti/2/custom_rtti.cpp index 0c154421..c2b873c3 100644 --- a/doc/modules/ROOT/examples/custom_rtti/2/custom_rtti.cpp +++ b/doc/modules/ROOT/examples/custom_rtti/2/custom_rtti.cpp @@ -5,6 +5,9 @@ // clang-format off +#include +#include + // tag::classes[] struct Node { Node(unsigned type) : type(type) {} @@ -52,12 +55,22 @@ struct custom_rtti : boost::openmethod::policies::deferred_static_rtti { using type_id = boost::openmethod::type_id; // for brevity + template + inline static std::uintptr_t np_id = 0; + + inline static std::uintptr_t np_id_alloc = + std::numeric_limits::max(); + template static auto static_type() { if constexpr (is_polymorphic) { return reinterpret_cast(T::static_type); } else { - return reinterpret_cast(0); + if (np_id == 0) { + np_id = np_id_alloc--; + } + + return reinterpret_cast(np_id); } } diff --git a/doc/modules/ROOT/examples/rolex/8/main.cpp b/doc/modules/ROOT/examples/rolex/8/main.cpp new file mode 100644 index 00000000..0412cc0c --- /dev/null +++ b/doc/modules/ROOT/examples/rolex/8/main.cpp @@ -0,0 +1,116 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +// tag::content[] +#include +#include +#include + +struct Animal { + virtual ~Animal() = default; +}; + +struct Dog : Animal {}; +struct Cat : Animal {}; + +BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat); + +// tag::zoo[] +struct Zoo { + BOOST_OPENMETHOD_MEM( + poke, (boost::openmethod::virtual_ptr), std::string); +}; +// end::zoo[] + +// tag::zookeeper[] +class ZooKeeper { + BOOST_OPENMETHOD_OVERRIDE_MEM( + Zoo::poke, (boost::openmethod::virtual_ptr), std::string) { + return "bark"; + } + BOOST_OPENMETHOD_OVERRIDE_MEM( + Zoo::poke, (boost::openmethod::virtual_ptr), std::string) { + return "hiss"; + } +}; +// end::zookeeper[] + +class Payroll; + +struct Employee { + virtual ~Employee() = default; +}; + +struct Salesman : Employee { + double sales = 0.0; +}; + +// tag::pay[] +BOOST_OPENMETHOD( + pay, (Payroll & payroll, boost::openmethod::virtual_ptr), + double); +// end::pay[] + +// tag::payroll[] +class Payroll { + public: + double balance() const { + return balance_; + } + + private: + double balance_ = 1'000'000.0; + + void update_balance(double amount) { + // throw if balance would become negative + balance_ += amount; + } + + BOOST_OPENMETHOD_OVERRIDE_MEM( + pay, + (Payroll & payroll, boost::openmethod::virtual_ptr), + double) { + double amount = 5000.0; + payroll.update_balance(-amount); + return amount; + } + + BOOST_OPENMETHOD_OVERRIDE_MEM( + pay, + (Payroll & payroll, boost::openmethod::virtual_ptr emp), + double) { + using self = BOOST_OPENMETHOD_OVERRIDER_MEM( + Payroll, pay, + (Payroll&, boost::openmethod::virtual_ptr), double); + double base = self::method_type::next(payroll, emp); + double commission = emp->sales * 0.05; + payroll.update_balance(-commission); + return base + commission; + } +}; +// end::payroll[] + +// ...and let's not forget to register the classes +BOOST_OPENMETHOD_CLASSES(Employee, Salesman); + +// tag::main[] +int main() { + boost::openmethod::initialize(); + + Dog snoopy; + Cat felix; + std::cout << "poke dog: " << Zoo::poke(snoopy) << "\n"; // bark + std::cout << "poke cat: " << Zoo::poke(felix) << "\n"; // hiss + + Payroll payroll; + Employee bill; + Salesman bob; + bob.sales = 100'000.0; + + std::cout << "pay bill: $" << pay(payroll, bill) << "\n"; // $5000 + std::cout << "pay bob: $" << pay(payroll, bob) << "\n"; // 10000 + std::cout << "remaining balance: $" << payroll.balance() << "\n"; // $985000 +} +// end::main[] diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index 8b036b60..590d0c56 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -5,7 +5,7 @@ ** xref:smart_pointers.adoc[Smart Pointers] ** xref:headers.adoc[Header and Implementation Files] ** xref:namespaces.adoc[Namespaces] -** xref:friends.adoc[Friends] +** xref:privacy.adoc[Members and Friends] ** xref:multiple_dispatch.adoc[Multiple Dispatch] * Advanced Features ** xref:core_api.adoc[Core API] diff --git a/doc/modules/ROOT/pages/custom_rtti.adoc b/doc/modules/ROOT/pages/custom_rtti.adoc index 14f91586..050b712e 100644 --- a/doc/modules/ROOT/pages/custom_rtti.adoc +++ b/doc/modules/ROOT/pages/custom_rtti.adoc @@ -53,6 +53,20 @@ struct std_rtti : rtti { `virtual_ptr`{empty}s "final" constructs. It is also called to set `bad_call::method`. This function is required. ++ +It is asked for the type id of types that are not registered classes, and of +types that are not classes at all: non-virtual parameter types, function types, +and two of the library's own - a method, and the registrar that stands for an +overrider. Those ids are keys, not labels. `initialize` groups the copies of a +method that several modules each registered by the type id of the method, and +the copies of an overrider by the type id of its registrar; a policy that +answers with one shared value for every type it does not recognise makes +distinct methods, or distinct overriders, indistinguishable, and they are +silently merged. So `static_type` must return a different value for every +different type - see the example below. Across modules it must return the same +value for a given type in each of them, up to `type_index`. `dynamic_type` is +under no such obligation: it is only ever called on an instance of a registered +polymorphic class. * `dynamic_type` is used to read the dynamic type of a virtual argument. If only the `virtual_ptr` "final" constructs are used, or if @@ -116,12 +130,18 @@ not require Node to be polymorphic in the C++ sense. If we remove the virtual destructor and implement `value` as an open-method as well, the program will still work. +`static_type` has to answer for the types that are not `Node`{empty}s too, and +its answers have to be distinct, so it hands each one an id of its own on first +use, counting down from the largest value a `type_id` can hold. Counting down +keeps them clear of the small ids the `Node` hierarchy assigns itself counting +up. Returning a single value such as zero for all of them would compile and run, +and would quietly merge two methods, or two overriders of one method, into one. + The policy is quite minimal. It does not support virtual inheritance, because it does not provide a `dynamic_cast_ref` function. It would not produce good error or trace messages, because it does not provide a `type_name` function. Instead, it relies on the `type_name` inherited from cpp:rtti::defaults[]. It -renders types as adorned integers, e.g. "type_id(2)". All non-"polymorphic" -types would be rendered the same way, as "type_id(0)". +renders types as adorned integers, e.g. "type_id(2)". cpp:rtti::defaults[] also provides a default implementation for `type_index`, which simply returns its argument. diff --git a/doc/modules/ROOT/pages/friends.adoc b/doc/modules/ROOT/pages/friends.adoc deleted file mode 100644 index dbe82d44..00000000 --- a/doc/modules/ROOT/pages/friends.adoc +++ /dev/null @@ -1,49 +0,0 @@ -:example: ../examples/rolex - -[#friendship] - -Note;; This section uses overrider containers, described in the -xref:headers.adoc[Headers] section. - -`friend` is a controversial feature. OpenMethod aims to interact well with all -of C++, as much as feasible for a library, and leaves the choice of using -`friend`, or not, to the user. - -Let's consider yet another variation of the `pay` example. This time, we want to -update a `balance` variable in a `Payroll` class, when an employee is paid. Thus -we pass the payroll object to the `pay` method: - -[source,c++] ----- -include::{example}/5/main.cpp[tag=pay] ----- - -`BOOST_OPENMETHOD` declares an overrider container for `pay` in the current -namespace, even though it does not define any overrider by itself. We can thus -name the individual address containers in `friend` declarations. But note that -at this point, the containers have not been specialized yet! In particular, the -`fn` member function does not exist yet. Instead, we declare friendship to the -container itself: - -[source,c++] ----- -include::{example}/5/main.cpp[tag=payroll] ----- - -We can now implement the `pay` overriders: - -[source,c++] ----- -include::{example}/5/main.cpp[tag=overriders] ----- - -We can also declare friendship _en_ _masse_: - -[source,c++] ----- -include::{example}/6/main.cpp[tag=payroll] ----- - -Note, however, that this makes all the overriders of _any_ `pay` method, with -any signature, in the current namespace, friends of `Payroll`. Unfortunately, -C++ does not currently allow partial specialization of friend declarations. diff --git a/doc/modules/ROOT/pages/privacy.adoc b/doc/modules/ROOT/pages/privacy.adoc new file mode 100644 index 00000000..1d07bb41 --- /dev/null +++ b/doc/modules/ROOT/pages/privacy.adoc @@ -0,0 +1,110 @@ +:page-aliases: friends.adoc +:example: ../examples/rolex + +[#members] + +A method may itself be a `static` member function of a class, declared with +xref:reference:BOOST_OPENMETHOD_MEM.adoc[BOOST_OPENMETHOD_MEM] instead of +xref:reference:BOOST_OPENMETHOD.adoc[BOOST_OPENMETHOD]: + +[source,c++] +---- +include::{example}/8/main.cpp[tag=zoo] +---- + +`Zoo::poke` is called as `Zoo::poke(animal)`. There is no implicit object +parameter and no dispatch on `this` - dispatch still goes entirely by the +method's own virtual parameters, exactly as for a free method. `poke` may be +overloaded within `Zoo`, just as a free method may be overloaded at namespace +scope. + +An overrider, too, may be a `static` member function - of any class, not +necessarily the one the method belongs to - added with +xref:reference:BOOST_OPENMETHOD_OVERRIDE_MEM.adoc[BOOST_OPENMETHOD_OVERRIDE_MEM] +instead of xref:reference:BOOST_OPENMETHOD_OVERRIDE.adoc[BOOST_OPENMETHOD_OVERRIDE]: + +[source,c++] +---- +include::{example}/8/main.cpp[tag=zookeeper] +---- + +The first argument names the method being overridden - here `Zoo::poke` - and +may equally name a free method declared with +xref:reference:BOOST_OPENMETHOD.adoc[BOOST_OPENMETHOD]. + +Since a member overrider's class need not be the method's own, this is what +lets an overrider reach a class's private state with no `friend` declaration +at all: being a member is already enough access. Let's revisit the `pay` +example once more, updating a `balance` in a `Payroll` class: + +[source,c++] +---- +include::{example}/8/main.cpp[tag=pay] +---- + +[source,c++] +---- +include::{example}/8/main.cpp[tag=payroll] +---- + +`update_balance` is `private`, and both overriders are members of `Payroll`, +so they call it as an ordinary same-class private call - no `friend` in +sight. `next`/`has_next` are not available by name inside a `_MEM` overrider's +body the way they are in a free one's; the second overrider reaches the first +through the core API instead, via +xref:reference:BOOST_OPENMETHOD_OVERRIDER_MEM.adoc[BOOST_OPENMETHOD_OVERRIDER_MEM], +passing it the class, method name, parameter list and return type of the +overrider doing the call - naming *itself*, not the overrider it calls. + +This does not apply when the class the overrider needs access to is not one +the caller controls - a third-party type with no room to add a member. The +rest of this page covers `friend`, which remains the way to grant access in +that case. + +[#friendship] + +Note;; This section uses overrider containers, described in the +xref:headers.adoc[Headers] section. + +`friend` is a controversial feature. OpenMethod aims to interact well with all +of C++, as much as feasible for a library, and leaves the choice of using +`friend`, or not, to the user. + +Let's consider yet another variation of the `pay` example. This time, we want to +update a `balance` variable in a `Payroll` class, when an employee is paid. Thus +we pass the payroll object to the `pay` method: + +[source,c++] +---- +include::{example}/5/main.cpp[tag=pay] +---- + +xref:reference:BOOST_OPENMETHOD.adoc[BOOST_OPENMETHOD] declares an overrider +container for `pay` in the current namespace, even though it does not define +any overrider by itself. We can thus name the individual address containers +in `friend` declarations. But note that at this point, the containers have +not been specialized yet! In particular, the `fn` member function does not +exist yet. Instead, we declare friendship to the container itself: + +[source,c++] +---- +include::{example}/5/main.cpp[tag=payroll] +---- + +We can now implement the `pay` overriders: + +[source,c++] +---- +include::{example}/5/main.cpp[tag=overriders] +---- + +We can also declare friendship _en_ _masse_: + +[source,c++] +---- +include::{example}/6/main.cpp[tag=payroll] +---- + +Note, however, that this makes all the overriders of _any_ `pay` method, with +any signature, in the current namespace, friends of `Payroll`. Unfortunately, +C++ does not currently allow partial specialization of friend declarations. diff --git a/doc/modules/ROOT/pages/ref_macros.adoc b/doc/modules/ROOT/pages/ref_macros.adoc index cf1513cd..60bc9930 100644 --- a/doc/modules/ROOT/pages/ref_macros.adoc +++ b/doc/modules/ROOT/pages/ref_macros.adoc @@ -22,6 +22,14 @@ uses of the library. | Declares a method overrider. | xref:reference:BOOST_OPENMETHOD_DEFINE_OVERRIDER.adoc[BOOST_OPENMETHOD_DEFINE_OVERRIDER] | Defines the body of a method overrider. +| xref:reference:BOOST_OPENMETHOD_MEM.adoc[BOOST_OPENMETHOD_MEM] +| Declares a method as a static member function. +| xref:reference:BOOST_OPENMETHOD_OVERRIDE_MEM.adoc[BOOST_OPENMETHOD_OVERRIDE_MEM] +| Adds an overrider, as a static member function, to a method. +| xref:reference:BOOST_OPENMETHOD_DECLARE_OVERRIDER_MEM.adoc[BOOST_OPENMETHOD_DECLARE_OVERRIDER_MEM] +| Declares a member overrider, without defining it. +| xref:reference:BOOST_OPENMETHOD_DEFINE_OVERRIDER_MEM.adoc[BOOST_OPENMETHOD_DEFINE_OVERRIDER_MEM] +| Defines the body of a member overrider. | xref:reference:BOOST_OPENMETHOD_ENABLE_RUNTIME_CHECKS.adoc[BOOST_OPENMETHOD_ENABLE_RUNTIME_CHECKS] | Enables runtime checks in method calls. |=== @@ -37,12 +45,16 @@ The following macros are for advanced uses of the library. | Default registry. | xref:reference:BOOST_OPENMETHOD_OVERRIDER.adoc[BOOST_OPENMETHOD_OVERRIDER] | Returns the class template specialization containing an overrider. +| xref:reference:BOOST_OPENMETHOD_OVERRIDER_MEM.adoc[BOOST_OPENMETHOD_OVERRIDER_MEM] +| Finds a member overrider. | xref:reference:BOOST_OPENMETHOD_OVERRIDERS.adoc[BOOST_OPENMETHOD_OVERRIDERS] | Returns the class template containing the overriders for all the methods with a given name. | xref:reference:BOOST_OPENMETHOD_ID.adoc[BOOST_OPENMETHOD_ID] | Generates a method id. | xref:reference:BOOST_OPENMETHOD_TYPE.adoc[BOOST_OPENMETHOD_TYPE] | Expands to core `method` specialization. +| xref:reference:BOOST_OPENMETHOD_TYPE_MEM.adoc[BOOST_OPENMETHOD_TYPE_MEM] +| Expands to a core `method` specialization, for a method declared with BOOST_OPENMETHOD_MEM. | xref:reference:BOOST_OPENMETHOD_REGISTER.adoc[BOOST_OPENMETHOD_REGISTER] | Creates a registrar object. | xref:reference:BOOST_OPENMETHOD_IMPORT_REGISTRY.adoc[BOOST_OPENMETHOD_IMPORT_REGISTRY] diff --git a/doc/modules/ROOT/snippets/member.cpp b/doc/modules/ROOT/snippets/member.cpp new file mode 100644 index 00000000..fffde3a4 --- /dev/null +++ b/doc/modules/ROOT/snippets/member.cpp @@ -0,0 +1,66 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +#include + +#include +#include + +#define BOOST_TEST_MODULE openmethod +#include + +#include "capture.hpp" + +using namespace boost::openmethod; + +struct Animal { + virtual ~Animal() = default; +}; +struct Cat : Animal {}; +struct Dog : Animal {}; + +BOOST_OPENMETHOD_CLASSES(Animal, Cat, Dog); + +// tag::declare[] +struct Zoo { + BOOST_OPENMETHOD_MEM( + poke, (virtual_ptr animal, std::ostream& os), void); +}; +// end::declare[] + +// tag::override[] +class Keeper { + BOOST_OPENMETHOD_OVERRIDE_MEM( + Zoo::poke, (virtual_ptr animal, std::ostream& os), void) { + (void)animal; + os << "hiss"; + } + + BOOST_OPENMETHOD_OVERRIDE_MEM( + Zoo::poke, (virtual_ptr animal, std::ostream& os), void) { + (void)animal; + os << "bark"; + } +}; +// end::override[] + +BOOST_AUTO_TEST_CASE(member_method_examples) { + initialize(); + + capture_cout cout; + + // tag::call[] + Cat felix; + Animal& a = felix; + Dog snoopy; + Animal& b = snoopy; + + Zoo::poke(a, std::cout); // hiss + Zoo::poke(b, std::cout); // bark + // end::call[] + + BOOST_TEST(cout.str() == "hissbark"); +} diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index efbfc176..71f16477 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -1018,9 +1018,8 @@ decltype(auto) acquire_vptr(const ArgType& arg) { Registry::require_initialized(); - if constexpr (has_vptr< - virtual_traits, - const ArgType&>) { + if constexpr ( + has_vptr, const ArgType&>) { return virtual_traits::vptr(arg); } else { return Registry::template policy::dynamic_vptr(arg); @@ -2728,8 +2727,8 @@ class method : template auto resolve_multi_first( - const ArgType& arg, - const MoreArgTypes&... more_args) const -> detail::word; + const ArgType& arg, const MoreArgTypes&... more_args) const + -> detail::word; template< std::size_t VirtualArg, typename MethodArgList, typename ArgType, @@ -2875,8 +2874,9 @@ method::operator()( using namespace detail; auto pf = resolve(args...); - return pf(std::forward::type>( - args)...); + return pf( + std::forward::type>( + args)...); } template< @@ -2915,9 +2915,9 @@ BOOST_FORCEINLINE auto method::vptr( if constexpr (detail::has_vptr_fn) { return boost_openmethod_vptr(obj, static_cast(nullptr)); - } else if constexpr (detail::has_vptr< - virtual_traits, - decltype(obj)>) { + } else if constexpr ( + detail::has_vptr< + virtual_traits, decltype(obj)>) { return virtual_traits::vptr(obj); } else { return Registry::template policy::dynamic_vptr(obj); @@ -2930,8 +2930,8 @@ template< template BOOST_FORCEINLINE auto method::resolve_uni( - const ArgType& arg, - const MoreArgTypes&... more_args) const -> detail::word { + const ArgType& arg, const MoreArgTypes&... more_args) const + -> detail::word { using namespace detail; using namespace policies; @@ -2950,8 +2950,8 @@ template< template BOOST_FORCEINLINE auto method::resolve_multi_first( - const ArgType& arg, - const MoreArgTypes&... more_args) const -> detail::word { + const ArgType& arg, const MoreArgTypes&... more_args) const + -> detail::word { using namespace detail; using namespace boost::mp11; @@ -3308,6 +3308,10 @@ void method::override_impl< this->return_type = Registry::rtti::template static_type< virtual_type>(); this->type = Registry::rtti::template static_type(); + // The registrar's own type: it carries `Function` as a non-type template + // argument, so it names this overrider and no other. + this->identity = Registry::rtti::template static_type< + std::remove_reference_t>(); using Thunk = thunk; detail:: init_type_ids::fn( diff --git a/include/boost/openmethod/initialize.hpp b/include/boost/openmethod/initialize.hpp index 120cb585..67f24afd 100644 --- a/include/boost/openmethod/initialize.hpp +++ b/include/boost/openmethod/initialize.hpp @@ -509,6 +509,13 @@ struct generic_compiler { } std::deque methods; + // (kept, duplicate) for every overrider copy that augment_methods() + // consolidated away. Each module has its own `next` variable for an + // overrider it registered, and dispatch through that module reads it, so + // the dropped copies' variables are filled from the kept one once its + // value is known. + std::vector> + overrider_copies; std::size_t class_mark = 0; bool compilation_done = false; }; @@ -1241,31 +1248,31 @@ void registry::compiler::augment_methods() { } // Collect overriders from every module copy of this method, deduping - // by *logical* identity rather than pointer identity - but only for - // overriders declared inline_. The same overrider, defined in a - // header and registered by two or more state-sharing modules (e.g. - // an exe and a DLL), appears once per module as a distinct - // overrider_info object - different address, and a different `pf` - // (each module compiles its own copy of the function) - but they - // share the same function type id and the same virtual-parameter - // type ids. Keeping every copy would make each dispatch cell they + // on overrider_info::identity - the type id of the registrar itself, + // which names the overrider rather than describing its shape. The + // same overrider, defined in a header and registered by two or more + // state-sharing modules (an exe and a DLL, say), appears once per + // module as a distinct overrider_info - different address, different + // `pf`, since each module compiles its own copy of the function - but + // one identity. Keeping every copy would make each dispatch cell they // fill ambiguous, because is_more_specific() reports "not more // specific" both ways for identical vp lists. // - // A NON-inline overrider with the same signature must never be - // merged, even if it happens to match: BOOST_OPENMETHOD_OVERRIDE - // (non-inline) keys one explicit specialization per signature, so - // writing it twice in one translation unit is a redefinition error, - // and defining it identically in more than one TU (as required for - // it to appear "duplicated" in the first place) is an ODR violation - // for a non-inline function - i.e. the situation this dedup exists - // to handle can only arise legitimately for `inline` overriders (see - // BOOST_OPENMETHOD_INLINE_OVERRIDE, which is the only thing that - // sets overrider_info::inline_ = true). Two DIFFERENT overriders - // sharing a signature (e.g. registered directly via - // method<...>::override and method<...>::override, both - // non-inline by default) are always genuinely distinct and must - // remain ambiguous. + // The inline_ flag and the signature comparison are necessary but + // not sufficient: a signature is a shape, and two genuinely different + // overriders share it routinely. Two BOOST_OPENMETHOD_INLINE_OVERRIDEs + // of one method in different namespaces were silently merged, one of + // them winning, where the non-inline spelling correctly reports the + // pair ambiguous. `identity` is the discriminator that was missing. + // + // It refines the test rather than replacing it, because it cannot be + // trusted alone: it is a type id of a non-class type, and an RTTI + // policy is free to return one sentinel for every type it does not + // recognize - the custom_rtti examples return 0. Such a policy makes + // every identity compare equal, so identity must never be the only + // thing keeping two overriders apart. As a conjunct it is safe: under + // a sentinel policy the behaviour is exactly what it was before, and + // under a well-behaved one the false merge is gone. std::vector all_specs; std::size_t module_index = 0; @@ -1279,6 +1286,19 @@ void registry::compiler::augment_methods() { return false; } + // Same overrider, not merely the same shape. Under a + // well-behaved RTTI policy this is what separates two + // different overriders that happen to share a signature + // from the several copies of one. Under a policy that + // ids every non-class type with one sentinel value (the + // custom_rtti examples do) every identity compares + // equal, and the conditions below carry the decision, + // exactly as they did before identity existed. + if (rtti::type_index(kept->identity) != + rtti::type_index(spec.identity)) { + return false; + } + if (rtti::type_index(kept->type) != rtti::type_index(spec.type)) { return false; @@ -1300,8 +1320,17 @@ void registry::compiler::augment_methods() { return true; }; - if (std::none_of(all_specs.begin(), all_specs.end(), same)) { + auto found = + std::find_if(all_specs.begin(), all_specs.end(), same); + + if (found == all_specs.end()) { all_specs.push_back(&spec); + } else { + // A second module's copy of an overrider already kept. + // Its own `next` variable still has to be filled, or + // next called through that module reads a null + // pointer while has_next() reports true. + overrider_copies.emplace_back(*found, &spec); } } } @@ -2030,6 +2059,16 @@ void registry::compiler::commit_global_data( } } + // Every module's copy of a consolidated overrider needs the same `next` + // as the copy that was kept: next resolves to the variable of the + // module it is called from, and a copy that was dropped during + // consolidation would otherwise still hold its zero-initialized value, + // null, while has_next() - which tests only for the not_implemented + // and ambiguous thunks - reports true. + for (auto [kept, duplicate] : overrider_copies) { + *duplicate->next = *kept->next; + } + for (auto& cls : classes) { for (auto& ci : cls.ci) { *ci->static_vptr = cls.vptr; diff --git a/include/boost/openmethod/macros.hpp b/include/boost/openmethod/macros.hpp index 4e2401be..724075e3 100644 --- a/include/boost/openmethod/macros.hpp +++ b/include/boost/openmethod/macros.hpp @@ -64,6 +64,47 @@ struct va_args { using method_type = method; }; +// The return type alone, for the macros that must not be given a registry: +// the ones that *name* an existing member method, and the ones that declare +// an overrider. A member method's registry is fixed by the declaration that +// created it, and an overrider takes the registry of the method it overrides, +// so a trailing registry there is always a mistake. Worth catching rather +// than tolerating: pasted raw into a function pointer type, as these macros +// once did, it produced a parse error on the user's own line with nothing to +// suggest the cause. A return type containing commas still arrives as several +// macro arguments but reassembles into one template argument, which is what +// tells the two cases apart. `return_type` is still published in the failing +// case, so one mistake yields one diagnostic instead of a cascade. +template +struct va_args_return_type { + using return_type = ReturnType; +}; + +template +struct va_args_no_registry; + +template +struct va_args_no_registry : va_args_return_type {}; + +template +struct va_args_no_registry : + va_args_return_type { + static_assert( + false_t, + "unexpected argument after the return type: a member method's " + "registry is fixed by its declaration, and an overrider takes the " + "registry of the method it overrides"); +}; + +// The registrar for a _MEM overrider, as a namespace-scope variable template +// rather than a class member - the device inplace_vptr_base uses for +// inplace_vptr_use_classes. Keyed on its own type, so it needs no invented +// name, and being `inline` it is one entity program-wide: the same overrider, +// seen from any number of translation units, registers exactly once, by +// linkage rather than by the inline_ dedup at initialize() time. +template +inline Registrar mem_registrar; + template inline constexpr bool method_not_found = false; @@ -71,6 +112,18 @@ inline constexpr bool method_not_found = false; #define BOOST_OPENMETHOD_GENSYM BOOST_PP_CAT(openmethod_gensym_, __COUNTER__) +// A name unique within a header, and - unlike BOOST_OPENMETHOD_GENSYM - the +// SAME in every translation unit that includes it. The _MEM macros declare +// class *members*, so a __COUNTER__-derived name gives a class in a header a +// different member-specification per TU, which is an ODR violation no compiler +// diagnoses and which makes a member method's type differ between TUs. __LINE__ +// is stable for a given header; two _MEM macros on one line collide loudly +// ("member declared twice"), never silently. The free macros are unaffected: +// their only gensym names a namespace-scope alias or an internal-linkage +// variable, neither of which is part of a class's member-specification. +#define BOOST_OPENMETHOD_DETAIL_LINESYM(PREFIX) \ + BOOST_PP_CAT(BOOST_PP_CAT(openmethod_, PREFIX), __LINE__) + //! Create a registrar object. //! //! Creates a registrar for a type, i.e. a static object of that type with a @@ -528,7 +581,7 @@ inline constexpr bool method_not_found = false; //! @see [Methods and Overriders](xref:ROOT:basics.adoc) //! @see [Header and Implementation Files](xref:ROOT:headers.adoc) //! @see [Namespaces](xref:ROOT:namespaces.adoc) -//! @see [Friends](xref:ROOT:friends.adoc) +//! @see [Members and Friends](xref:ROOT:privacy.adoc) #define BOOST_OPENMETHOD_OVERRIDE(ID, PARAMETERS, ...) \ BOOST_OPENMETHOD_DECLARE_OVERRIDER(ID, PARAMETERS, __VA_ARGS__) \ BOOST_OPENMETHOD_DEFINE_OVERRIDER(ID, PARAMETERS, __VA_ARGS__) @@ -583,6 +636,266 @@ inline constexpr bool method_not_found = false; ID, PARAMETERS, __VA_ARGS__)::fn PARAMETERS \ -> boost::mp11::mp_back> +#define BOOST_OPENMETHOD_DETAIL_MEM(TAG, ALIAS, ID, PARAMETERS, ...) \ + struct TAG; \ + using ALIAS = \ + ::boost::openmethod::detail::va_args<__VA_ARGS__>::method_type< \ + TAG, \ + ::boost::openmethod::detail::va_args<__VA_ARGS__>::return_type \ + PARAMETERS>; \ + static auto BOOST_OPENMETHOD_ID(ID)( \ + ::boost::openmethod::detail::va_args<__VA_ARGS__>::return_type(*) \ + PARAMETERS) \ + ->ALIAS; \ + template \ + static typename ::boost::openmethod::detail::enable_forwarder< \ + void, ALIAS, \ + ::boost::openmethod::detail::va_args<__VA_ARGS__>::return_type, \ + ForwarderParameters...>::type \ + ID(ForwarderParameters&&... args) { \ + return ALIAS::fn(std::forward(args)...); \ + } \ + template \ + static \ + typename ::boost::openmethod::detail::enable_guide_ignoring_registry< \ + void, ALIAS, ALIAS, ForwarderParameters...>::type \ + BOOST_OPENMETHOD_DETAIL_GUIDE_ANY_REGISTRY(ID)( \ + ForwarderParameters && ... args); \ + template \ + static typename ::boost::openmethod::detail::enable_forwarder< \ + void, ALIAS, ALIAS, ForwarderParameters...>::type \ + BOOST_OPENMETHOD_GUIDE(ID)(ForwarderParameters && ... args) + +//! Declare a method as a static member function. +//! +//! `BOOST_OPENMETHOD_MEM` performs the same function as @ref BOOST_OPENMETHOD, +//! except that it declares a `static` member function of the class whose body +//! it is used in, instead of a free function. There is no implicit object +//! parameter and no dispatch on `this`; the method's own virtual parameters +//! decide dispatch exactly as for a free method. +//! +//! The method is called as `Class::ID(args...)`. `ID` may be overloaded within +//! the class, just as a free method may be overloaded at namespace scope. +//! +//! Unlike @ref BOOST_OPENMETHOD, this macro does not create an overrider +//! container, so an overrider for this method must be added with +//! @ref BOOST_OPENMETHOD_OVERRIDE_MEM, or with +//! @ref BOOST_OPENMETHOD_DECLARE_OVERRIDER_MEM and +//! @ref BOOST_OPENMETHOD_DEFINE_OVERRIDER_MEM, never with the free overrider +//! macros. +//! +//! @note `ID` must be an *identifier*. Qualified names are not allowed - it +//! names a member of the class the macro is used in, not the method being +//! overridden. +//! +//! @par Example +//! +//! include:member.cpp#declare;override;call +//! +//! @param ID The method's name. +//! @param PARAMETERS The method's parameter list, in parentheses. +//! @param ... The method's return type, optionally followed by the registry. +//! +//! @see [Members and Friends](xref:ROOT:privacy.adoc) +#define BOOST_OPENMETHOD_MEM(ID, PARAMETERS, ...) \ + BOOST_OPENMETHOD_DETAIL_MEM( \ + BOOST_OPENMETHOD_DETAIL_LINESYM(tag_), \ + BOOST_OPENMETHOD_DETAIL_LINESYM(alias_), ID, PARAMETERS, __VA_ARGS__) + +//! Expand to a core `method` specialization, for a method declared with +//! @ref BOOST_OPENMETHOD_MEM. +//! +//! @ref BOOST_OPENMETHOD_TYPE cannot name a member method: it reconstructs the +//! method's identifier tag from `ID` alone, and a member method's tag is a +//! generated name, not `ID`-derived, so that `ID` may be overloaded within its +//! class. `BOOST_OPENMETHOD_TYPE_MEM` looks the type up instead, through a +//! function declared for exactly that purpose by `BOOST_OPENMETHOD_MEM`. +//! +//! @note There is no registry argument: the method's registry was fixed when +//! it was declared with @ref BOOST_OPENMETHOD_MEM. +//! +//! @param ID The method's name, qualified with its class, e.g. `Zoo::poke`. +//! @param PARAMETERS The method's parameter list, in parentheses. +//! @param ... The method's return type. No registry may follow it: a member +//! method's registry is fixed by the declaration that created it. +//! +//! @see [Members and Friends](xref:ROOT:privacy.adoc) +#define BOOST_OPENMETHOD_TYPE_MEM(ID, PARAMETERS, ...) \ + decltype(BOOST_OPENMETHOD_ID(ID)( \ + static_cast::return_type(*) PARAMETERS>(nullptr))) + +// The overrider's body cannot be named after ID: ID may be qualified (e.g. +// Zoo::poke, to override a member method), and pasting a qualified name into +// a *new* declaration is ill-formed - `##` only joins the token next to it, +// so ID##_boost_openmethod is `Zoo`, `::`, `poke_boost_openmethod`, a valid +// call target but never a valid declared name inside an unrelated class. So +// the body and the accessor that finds its key each get one fixed name, +// overloaded purely on the exact `RET (*) PARAMETERS` - see the reference +// page for what that costs (at most one overrider of that exact signature per +// class, regardless of which method it overrides). +// +// KEY is the one gensym this needs. Its trampoline is a member function +// template defined inline inside KEY, itself nested inside the class this +// macro is used in: its body, referring to the overrider declared after it, +// is resolved in KEY's own complete-class context, which (like the library's +// existing free-overrider machinery) extends through the nesting to the +// enclosing class. Two things that look simpler do not work, on either +// compiler: defining the overrider inside KEY and reaching it through +// `decltype(...)::fn` - decltype cannot name a declaration that way - and +// defining KEY's own member out-of-line while still inside the enclosing +// class - illegal for a nested class. The overrider is therefore an ordinary +// member of the enclosing class, not of KEY. +// The trampoline is reached only through `fn`, the function pointer formed from +// its address on the next line, so at an explicit `fn(args)` call it cannot be +// inlined. BOOST_FORCEINLINE still pays on gcc and clang, where it removes the +// extra call in an unoptimized build. On MSVC it pays nothing - __forceinline is +// ignored under /Od - and costs a hard error everywhere else: /O2 emits C4714, +// "marked as __forceinline not inlined", which taking the address guarantees, and +// the suite builds with /W4 /WX. Guarded on BOOST_MSVC, not _MSC_VER, so clang-cl +// (which does not implement C4714) keeps the attribute. +#ifdef BOOST_MSVC +#define BOOST_OPENMETHOD_DETAIL_TRAMPOLINE_INLINE +#else +#define BOOST_OPENMETHOD_DETAIL_TRAMPOLINE_INLINE BOOST_FORCEINLINE +#endif + +#define BOOST_OPENMETHOD_DETAIL_OVERRIDE_MEM( \ + KEY, REGISTRAR, ID, PARAMETERS, ...) \ + BOOST_OPENMETHOD_DETAIL_OVERRIDE_MEM_AUX( \ + KEY, REGISTRAR, ID, PARAMETERS, \ + typename ::boost::openmethod::detail::va_args_no_registry< \ + __VA_ARGS__>::return_type) + +#define BOOST_OPENMETHOD_DETAIL_OVERRIDE_MEM_AUX( \ + KEY, REGISTRAR, ID, PARAMETERS, ...) \ + struct KEY { \ + BOOST_OPENMETHOD_DETAIL_LOCATE_METHOD(ID, PARAMETERS); \ + using method_type = \ + boost_openmethod_detail_locate_method_aux::type; \ + template \ + static BOOST_OPENMETHOD_DETAIL_TRAMPOLINE_INLINE auto trampoline( \ + ForwarderParameters... args) -> __VA_ARGS__ { \ + (void)&::boost::openmethod::detail::mem_registrar< \ + typename KEY::method_type::REGISTRAR>; \ + return boost_openmethod_overrider_body( \ + static_cast(args)...); \ + } \ + static constexpr __VA_ARGS__(*fn) \ + PARAMETERS = static_cast<__VA_ARGS__(*) PARAMETERS>(&trampoline); \ + }; \ + static auto boost_openmethod_overrider_key(__VA_ARGS__(*) PARAMETERS) \ + ->KEY; \ + static auto boost_openmethod_overrider_body PARAMETERS->__VA_ARGS__ + +//! Add an overrider, as a static member function, to a method. +//! +//! `BOOST_OPENMETHOD_OVERRIDE_MEM` performs the same function as +//! @ref BOOST_OPENMETHOD_OVERRIDE, except that the overrider is a `static` +//! member function of the class whose body it is used in - which, being a +//! member, has the same access to that class's private state as any other +//! member, with no need to `friend` anything. +//! +//! `ID` names the method being overridden, and may be a free method declared +//! with @ref BOOST_OPENMETHOD or a member method declared with +//! @ref BOOST_OPENMETHOD_MEM, qualified with its class (`Zoo::poke`). +//! +//! It is followed by the overrider's body, exactly like +//! @ref BOOST_OPENMETHOD_OVERRIDE. +//! +//! @note Neither `next` nor `has_next` is available by name inside the body. +//! Reach them through the core API instead: name the overrider itself with +//! @ref BOOST_OPENMETHOD_OVERRIDER_MEM, then call `method_type::next` +//! and `method_type::has_next` on it. +//! +//! @par Example +//! +//! include:member.cpp#declare;override;call +//! +//! @param ID The method's name. +//! @param PARAMETERS The overrider's parameter list, in parentheses. +//! @param ... The overrider's return type. No registry may follow it: an +//! overrider takes the registry of the method it overrides. +//! +//! @see [Members and Friends](xref:ROOT:privacy.adoc) +#define BOOST_OPENMETHOD_OVERRIDE_MEM(ID, PARAMETERS, ...) \ + BOOST_OPENMETHOD_DETAIL_OVERRIDE_MEM( \ + BOOST_OPENMETHOD_DETAIL_LINESYM(key_), inline_override, ID, \ + PARAMETERS, __VA_ARGS__) + +//! Declare a member overrider, without defining it. +//! +//! Performs the same function as @ref BOOST_OPENMETHOD_OVERRIDE_MEM, but does +//! not start the overrider's definition - use +//! @ref BOOST_OPENMETHOD_DEFINE_OVERRIDER_MEM for that, in an implementation +//! file. +//! +//! @param ID The method's name. +//! @param PARAMETERS The overrider's parameter list, in parentheses. +//! @param ... The overrider's return type. No registry may follow it: an +//! overrider takes the registry of the method it overrides. +//! +//! @see [Members and Friends](xref:ROOT:privacy.adoc) +#define BOOST_OPENMETHOD_DECLARE_OVERRIDER_MEM(ID, PARAMETERS, ...) \ + BOOST_OPENMETHOD_DETAIL_OVERRIDE_MEM( \ + BOOST_OPENMETHOD_DETAIL_LINESYM(key_), override, ID, PARAMETERS, \ + __VA_ARGS__) + +//! Define the body of a member overrider declared with +//! @ref BOOST_OPENMETHOD_DECLARE_OVERRIDER_MEM. +//! +//! Used at namespace scope, followed by the overrider's body. +//! +//! @param CLASS The class @ref BOOST_OPENMETHOD_DECLARE_OVERRIDER_MEM was used +//! in. +//! @param ID The method's name. +//! @param PARAMETERS The overrider's parameter list, in parentheses. +//! @param ... The overrider's return type. No registry may follow it: an +//! overrider takes the registry of the method it overrides. +//! +//! @see [Members and Friends](xref:ROOT:privacy.adoc) +#define BOOST_OPENMETHOD_DEFINE_OVERRIDER_MEM(CLASS, ID, PARAMETERS, ...) \ + auto CLASS::boost_openmethod_overrider_body PARAMETERS-> \ + typename ::boost::openmethod::detail::va_args_no_registry< \ + __VA_ARGS__>::return_type + +//! Find a member overrider. +//! +//! Expands to the type that holds a member overrider added with +//! @ref BOOST_OPENMETHOD_OVERRIDE_MEM or +//! @ref BOOST_OPENMETHOD_DECLARE_OVERRIDER_MEM. It has two members: +//! +//! @li `fn`: a pointer to a function, the overrider's registered address. Can +//! be called directly, with no dispatch: `BOOST_OPENMETHOD_OVERRIDER_MEM( +//! Class, ID, PARAMETERS, ...)::fn(args...)`. +//! +//! @li `method_type`: the overridden method's own type, so that +//! `method_type::next(args...)` and `method_type::has_next()` are +//! available from the core API. +//! +//! @note Unlike @ref BOOST_OPENMETHOD_OVERRIDER, `fn` here is a pointer, not a +//! function: the registered address belongs to a compiler-generated +//! forwarding function, not to the overrider's body directly. `next`, +//! `has_next` and `fn(args)` all still work as expected. +//! +//! @note `ID` plays no part in finding the overrider - only `CLASS`, +//! `PARAMETERS` and the return type do, since a `_MEM` overrider is looked up +//! by its exact signature within `CLASS`, regardless of which method it +//! overrides. `ID` is required for a uniform call shape across the `_MEM` +//! overrider macros, not because this one needs it. +//! +//! @param CLASS The class the overrider was added to. +//! @param ID The method's name. +//! @param PARAMETERS The overrider's parameter list, in parentheses. +//! @param ... The overrider's return type. No registry may follow it: an +//! overrider takes the registry of the method it overrides. +//! +//! @see [Members and Friends](xref:ROOT:privacy.adoc) +#define BOOST_OPENMETHOD_OVERRIDER_MEM(CLASS, ID, PARAMETERS, ...) \ + decltype(CLASS::boost_openmethod_overrider_key( \ + static_cast::return_type(*) PARAMETERS>(nullptr))) + //! Register classes. //! //! Registers classes in a registry. diff --git a/include/boost/openmethod/preamble.hpp b/include/boost/openmethod/preamble.hpp index c1f6f24f..06e98f29 100644 --- a/include/boost/openmethod/preamble.hpp +++ b/include/boost/openmethod/preamble.hpp @@ -390,6 +390,15 @@ struct overrider_info : static_list::static_link { method_info* method; // for the destructor, to remove definition type_id return_type; // for N2216 disambiguation type_id type; // of the function, for trace + // Which overrider this *is*, as opposed to what it looks like. `type` is + // the function type, RET(PARAMS) - a shape two unrelated overriders + // routinely share. `identity` is the type id of the registrar itself, + // whose mangled name embeds the registered function as a non-type + // template argument, so it is distinct between any two different + // overriders and identical between the copies of one overrider that + // several modules each register. augment_methods() consolidates on it, + // exactly as it consolidates method copies on method_type_id. + type_id identity; void (**next)(); type_id *vp_begin, *vp_end; void (*pf)(); @@ -590,9 +599,22 @@ struct RttiFn { //! Returns the static @ref type_id of a type. //! - //! @note `Class` is not necessarily a @e registered class. This - //! function is also called to acquire the type_id of non-virtual - //! parameters, library types, etc, for diagnostic and trace purposes. + //! `Class` is not necessarily a @e registered class, nor even a class. + //! The library also asks for the type_id of non-virtual parameters, + //! function types, and its own internal types - a method, and the + //! registrar standing for an overrider. + //! + //! @warning Those type_ids are not merely descriptive. @ref initialize + //! groups the per-module copies of a method by the type_id of the method + //! itself, and the copies of an overrider by the type_id of its + //! registrar, so a policy that answers with one shared value for every + //! type it does not recognize makes distinct methods, or distinct + //! overriders, indistinguishable. `static_type` must therefore return a + //! different value for every different type, whatever that type is. A + //! program that spans several modules needs more: the value must also be + //! the same in each of them for a given type, up to @ref type_index. + //! @ref dynamic_type carries no such obligation - it is only ever called + //! on an instance of a registered polymorphic class. //! //! @tparam Class A class. //! @return The static type_id of Class. @@ -603,7 +625,7 @@ struct RttiFn { //! //! @tparam Class A registered class. //! @param obj A reference to an instance of `Class`. - //! @return The type_id of `obj`'s class. + //! @return The type_id of the class of `obj`. template static auto dynamic_type(const Class& obj) -> type_id; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e493cb5e..aeec03aa 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -231,4 +231,5 @@ endif() # Implicitly (build-time) linked shared libraries. Needs no Boost::dll, since # nothing is loaded at run time. +add_subdirectory(cross_tu) add_subdirectory(implicit_shared_libraries) diff --git a/test/Jamfile b/test/Jamfile index 1281c244..fe8f11d8 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -112,6 +112,7 @@ for local src in [ glob compile_fail_*.cpp ] compile-fail $(src) ; } +build-project cross_tu ; build-project dynamic_loading ; build-project implicit_shared_libraries ; diff --git a/test/compile_fail_member_method_private.cpp b/test/compile_fail_member_method_private.cpp new file mode 100644 index 00000000..4ee0cf94 --- /dev/null +++ b/test/compile_fail_member_method_private.cpp @@ -0,0 +1,37 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +// Expected diagnostic, as a CMake regex (see CMakeLists.txt). +// expected-error: cannot find + +#include + +using namespace boost::openmethod; + +class Animal { + public: + virtual ~Animal() = default; +}; + +class Dog : public Animal {}; + +// BOOST_OPENMETHOD_MEM emits everything - the tag, the guides, the call +// forwarder - with the access of the section it is used in. A private +// member method's guide is therefore unreachable from outside the class: +// LOCATE_METHOD's ADL-based lookup is a substitution failure, not an access +// error, so it reports the same "cannot find" diagnostic as a genuinely +// missing method. +class Zoo { + BOOST_OPENMETHOD_MEM(poke, (virtual_ptr), void); +}; + +class Handler { + BOOST_OPENMETHOD_OVERRIDE_MEM(Zoo::poke, (virtual_ptr), void) { + } +}; + +int main() { + return 0; +} diff --git a/test/compile_fail_member_overrider_parameter_mismatch.cpp b/test/compile_fail_member_overrider_parameter_mismatch.cpp new file mode 100644 index 00000000..91a600ed --- /dev/null +++ b/test/compile_fail_member_overrider_parameter_mismatch.cpp @@ -0,0 +1,44 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +// Expected diagnostic, as a CMake regex (see CMakeLists.txt). +// expected-error: cannot find + +#include + +using namespace boost::openmethod; + +class Animal { + public: + virtual ~Animal() = default; +}; + +class Cat : private Animal { + public: + Animal& as_animal() { + return *this; + } +}; + +BOOST_OPENMETHOD(poke, (virtual_ptr), void); + +// A _MEM overrider is located the same way a free-macro overrider is - +// through BOOST_OPENMETHOD_DETAIL_LOCATE_METHOD, i.e. ADL/guide lookup, never +// through the core API's override directly. Since Cat privately inherits +// Animal, virtual_ptr does not convert to virtual_ptr from +// outside Cat, so even the registry-relaxed guide fails to match and the +// diagnosis stops at "cannot find" - the more specific "must be an +// unambiguous accessible base" (see +// compile_fail_virtual_parameter_private_base_core.cpp) is only reachable by +// registering through the core API directly, bypassing guide lookup +// entirely, which no _MEM macro does. +class Handler { + BOOST_OPENMETHOD_OVERRIDE_MEM(poke, (virtual_ptr), void) { + } +}; + +int main() { + return 0; +} diff --git a/test/compile_fail_member_overrider_registry_argument.cpp b/test/compile_fail_member_overrider_registry_argument.cpp new file mode 100644 index 00000000..d39e8e50 --- /dev/null +++ b/test/compile_fail_member_overrider_registry_argument.cpp @@ -0,0 +1,39 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +// Expected diagnostic, as a CMake regex (see CMakeLists.txt). +// expected-error: unexpected argument after the return type + +#include + +#include + +using namespace boost::openmethod; + +struct my_registry : default_registry::with<> {}; + +struct Animal { + virtual ~Animal() = default; +}; + +struct Dog : Animal {}; + +BOOST_OPENMETHOD_CLASSES(Animal, Dog, my_registry); + +// The method may name a registry... +BOOST_OPENMETHOD( + poke, (virtual_ptr), std::string, my_registry); + +class Keeper { + // ...but an overrider takes the method's, and naming one is an error. + BOOST_OPENMETHOD_OVERRIDE_MEM( + poke, (virtual_ptr), std::string, my_registry) { + return "bark"; + } +}; + +int main() { + return 0; +} diff --git a/test/compile_fail_member_overrider_signature_collision.cpp b/test/compile_fail_member_overrider_signature_collision.cpp new file mode 100644 index 00000000..1ccc43b4 --- /dev/null +++ b/test/compile_fail_member_overrider_signature_collision.cpp @@ -0,0 +1,47 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +// Expected diagnostic, as a CMake regex (see CMakeLists.txt). +// The three compilers word this differently - gcc "cannot be overloaded +// with", clang "class member cannot be redeclared", MSVC C2556 "overloaded +// function differs only by return type" - but all three name the accessor +// whose overload set the collision happens in, so match that. A bare `.*` +// would match anything, and since the test asserts only +// PASS_REGULAR_EXPRESSION - CTest ignores the exit status once that is set - +// it would pass even if the file compiled cleanly. +// expected-error: boost_openmethod_overrider_key + +#include + +using namespace boost::openmethod; + +class Animal { + public: + virtual ~Animal() = default; +}; + +class Dog : public Animal {}; + +BOOST_OPENMETHOD(pay, (virtual_ptr), double); +BOOST_OPENMETHOD(greet, (virtual_ptr), double); + +// A _MEM overrider's body and the accessor that finds its key are each +// named once, overloaded purely on the exact (return type, parameter list) - +// never on which method they override, since a qualified method name cannot +// be pasted into a new declaration (see macros.hpp). Two overriders of +// *different* methods, with an identical signature, therefore redeclare the +// same overload in one class. +class Handler { + BOOST_OPENMETHOD_OVERRIDE_MEM(pay, (virtual_ptr), double) { + return 1.0; + } + BOOST_OPENMETHOD_OVERRIDE_MEM(greet, (virtual_ptr), double) { + return 2.0; + } +}; + +int main() { + return 0; +} diff --git a/test/cross_tu/CMakeLists.txt b/test/cross_tu/CMakeLists.txt new file mode 100644 index 00000000..d632b0e4 --- /dev/null +++ b/test/cross_tu/CMakeLists.txt @@ -0,0 +1,27 @@ +# Copyright (c) 2017-2026 Jean-Louis Leroy +# Distributed under the Boost Software License, Version 1.0. +# See accompanying file LICENSE_1_0.txt +# or copy at http://www.boost.org/LICENSE_1_0.txt) + +# A member method, declared in a header, used from two translation units. +# +# Everything else in test/ is a single translation unit, which is why this +# shape went uncovered: the _MEM macros declare class members, so a name +# derived from __COUNTER__ makes a class in a header differ between TUs. That +# is an ODR violation no compiler diagnoses, and it is invisible to any test +# built from one .cpp. +# +# No PCH: main.cpp advances __COUNTER__ before including the header, and a +# force-included PCH would be parsed first. + +set(exe boost_openmethod-test_cross_tu) + +add_executable(${exe} EXCLUDE_FROM_ALL main.cpp other_tu.cpp) +target_link_libraries(${exe} + PRIVATE Boost::openmethod Boost::unit_test_framework) + +boost_openmethod_add_test(${exe}) + +if (TARGET tests) + add_dependencies(tests ${exe}) +endif() diff --git a/test/cross_tu/Jamfile b/test/cross_tu/Jamfile new file mode 100644 index 00000000..5ccc2339 --- /dev/null +++ b/test/cross_tu/Jamfile @@ -0,0 +1,24 @@ +# Boost.OpenMethod Library - test/cross_tu Jamfile +# +# Copyright 2017-2026 Jean-Louis Leroy +# +# Distributed under the Boost Software License, Version 1.0. +# See accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt + +# A member method declared in a header and used from two translation units. +# Every other test here is a single TU, so nothing else can catch a _MEM macro +# whose generated member names differ between TUs. +# +# The project requirements of test/Jamfile - including +# /boost/openmethod//boost_openmethod - propagate to this Jamfile. + +import testing ; + +run main.cpp other_tu.cpp + /boost/test//boost_unit_test_framework/off/static + : + : + : + : test_cross_tu + ; diff --git a/test/cross_tu/main.cpp b/test/cross_tu/main.cpp new file mode 100644 index 00000000..6d5c7c20 --- /dev/null +++ b/test/cross_tu/main.cpp @@ -0,0 +1,28 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +// Deliberately advance __COUNTER__ before the include, so that a _MEM macro +// naming its members with it would produce a Zoo, and a Zoo::poke, different +// from the ones other_tu.cpp sees. That compiles and links clean; the method +// then dispatches through a different method<> object than the one the +// overriders registered with, and the call reports "not implemented". +enum { advance_the_counter = __COUNTER__ }; + +#include "member_method.hpp" + +#include + +#define BOOST_TEST_MODULE cross_tu_member_method +#include + +BOOST_AUTO_TEST_CASE(member_method_across_translation_units) { + initialize(); + + Dog snoopy; + Cat felix; + + BOOST_TEST(Zoo::poke(snoopy) == "bark"); + BOOST_TEST(Zoo::poke(felix) == "hiss"); +} diff --git a/test/cross_tu/member_method.hpp b/test/cross_tu/member_method.hpp new file mode 100644 index 00000000..0fed661c --- /dev/null +++ b/test/cross_tu/member_method.hpp @@ -0,0 +1,39 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#ifndef BOOST_OPENMETHOD_TEST_CROSS_TU_MEMBER_METHOD_HPP +#define BOOST_OPENMETHOD_TEST_CROSS_TU_MEMBER_METHOD_HPP + +#include + +#include + +using namespace boost::openmethod; + +struct Animal { + virtual ~Animal() = default; +}; + +struct Dog : Animal {}; +struct Cat : Animal {}; + +// A member method has nowhere to live but a header, so everything the _MEM +// macros emit must be identical in every translation unit that includes this +// file. They declare class *members*, so a name derived from __COUNTER__ would +// give Zoo and Keeper a different member-specification per TU. +struct Zoo { + BOOST_OPENMETHOD_MEM(poke, (virtual_ptr), std::string); +}; + +// An overrider that is itself in the header, so both TUs see it. It must +// register exactly once: the registrar is a namespace-scope variable template +// keyed on its own type, hence one entity program-wide. +class Keeper { + BOOST_OPENMETHOD_OVERRIDE_MEM(Zoo::poke, (virtual_ptr), std::string) { + return "hiss"; + } +}; + +#endif diff --git a/test/cross_tu/other_tu.cpp b/test/cross_tu/other_tu.cpp new file mode 100644 index 00000000..7322a9da --- /dev/null +++ b/test/cross_tu/other_tu.cpp @@ -0,0 +1,19 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +// A second translation unit that adds an overrider to Zoo::poke. This is the +// half of the test that fails if Zoo::poke does not name the same method in +// both TUs: the overrider registers with this TU's Zoo::poke, and main.cpp +// calls its own. + +#include "member_method.hpp" + +BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat); + +class Trainer { + BOOST_OPENMETHOD_OVERRIDE_MEM(Zoo::poke, (virtual_ptr), std::string) { + return "bark"; + } +}; diff --git a/test/implicit_shared_libraries/CMakeLists.txt b/test/implicit_shared_libraries/CMakeLists.txt index e6877e56..8a9ac005 100644 --- a/test/implicit_shared_libraries/CMakeLists.txt +++ b/test/implicit_shared_libraries/CMakeLists.txt @@ -21,3 +21,4 @@ message(STATUS "Boost.OpenMethod: building implicit shared library tests") add_subdirectory(default_registry) add_subdirectory(custom_registry) +add_subdirectory(member_overrider) diff --git a/test/implicit_shared_libraries/Jamfile b/test/implicit_shared_libraries/Jamfile index 0f11d53d..631845b9 100644 --- a/test/implicit_shared_libraries/Jamfile +++ b/test/implicit_shared_libraries/Jamfile @@ -42,3 +42,4 @@ project build-project default_registry ; build-project custom_registry ; +build-project member_overrider ; diff --git a/test/implicit_shared_libraries/member_overrider/CMakeLists.txt b/test/implicit_shared_libraries/member_overrider/CMakeLists.txt new file mode 100644 index 00000000..0b4a06d5 --- /dev/null +++ b/test/implicit_shared_libraries/member_overrider/CMakeLists.txt @@ -0,0 +1,50 @@ +# Copyright (c) 2017-2026 Jean-Louis Leroy +# Distributed under the Boost Software License, Version 1.0. +# See accompanying file LICENSE_1_0.txt +# or copy at http://www.boost.org/LICENSE_1_0.txt) + +# A member method and a member overrider declared in a header that both the +# executable and the shared library include: the two copies of the overrider +# must be consolidated into one, not left ambiguous. + +set(lib boost_openmethod-isl_member_overrider_lib) +set(exe boost_openmethod-test_implicit_shared_libraries_member_overrider) + +# lib.cpp emits the one definition of the registry state: this library owns +# it, and the executable imports it. +add_library(${lib} SHARED lib.cpp) +target_link_libraries(${lib} PRIVATE Boost::openmethod) + +add_executable(${exe} main.cpp) +target_link_libraries(${exe} + PRIVATE Boost::openmethod Boost::unit_test_framework ${lib}) + +# Co-locate the library with the executable so it is found at run time on +# Windows, where there is no rpath. On ELF and Mach-O, CMake's default +# build-tree RPATH already resolves an implicitly linked library, but a shared +# library is a *library* output there and would otherwise stay in this +# subdirectory. +set_target_properties(${lib} PROPERTIES + LIBRARY_OUTPUT_DIRECTORY $ + RUNTIME_OUTPUT_DIRECTORY $) + +# Hidden visibility, which is what the Boost super-project builds with. It is +# the setting that makes this test worth having: at default visibility the +# registrar - an inline variable template - is merged across the two modules by +# the dynamic linker, only one registration survives, and nothing reaches the +# consolidation path at all. Hidden, each module keeps its own copy and +# initialize() has to recognise them as copies of one overrider. The registry +# state is shared through the EXPORT/IMPORT macros, which exist for exactly +# this configuration. +if (NOT WIN32) + foreach(t ${lib} ${exe}) + target_compile_options(${t} PRIVATE + -fvisibility=hidden -fvisibility-inlines-hidden) + endforeach() +endif() + +boost_openmethod_add_test(${exe}) + +if (TARGET tests) + add_dependencies(tests ${exe} ${lib}) +endif() diff --git a/test/implicit_shared_libraries/member_overrider/Jamfile b/test/implicit_shared_libraries/member_overrider/Jamfile new file mode 100644 index 00000000..d8bb5b0c --- /dev/null +++ b/test/implicit_shared_libraries/member_overrider/Jamfile @@ -0,0 +1,41 @@ +# Boost.OpenMethod Library - implicit_shared_libraries/default_registry Jamfile +# +# Copyright 2017-2026 Jean-Louis Leroy +# +# Distributed under the Boost Software License, Version 1.0. +# See accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt + +# default_registry shared across an implicitly linked library: +# +# test executable ──linked──▶ lib (owns and exports the registry state) +# +# Unlike test/dynamic_loading, nothing is loaded at run time, so there is no +# Boost.DLL dependency, no -rdynamic, and no need to place the library in a +# specific directory: b2 adds a linked shared library's directory to the test's +# runtime path. +# +# global is deliberately NOT set: the export/import macros are +# what make the state shareable under hidden visibility, and forcing global +# visibility would mask a regression in them. +# +# The project requirements of test/Jamfile - including +# /boost/openmethod//boost_openmethod - propagate to this Jamfile. + +import testing ; + +# lib.cpp emits the one definition of the registry state: this library owns +# it, and the executable imports it. +lib boost_openmethod-isl_member_overrider_lib + : lib.cpp + : shared + ; + +run main.cpp + boost_openmethod-isl_member_overrider_lib + /boost/test//boost_unit_test_framework/off/static + : + : + : shared + : implicit_shared_libraries_member_overrider + ; diff --git a/test/implicit_shared_libraries/member_overrider/lib.cpp b/test/implicit_shared_libraries/member_overrider/lib.cpp new file mode 100644 index 00000000..b668a56e --- /dev/null +++ b/test/implicit_shared_libraries/member_overrider/lib.cpp @@ -0,0 +1,26 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#define LIB_SOURCE +#define OWNS_REGISTRY_STATE + +#include "lib.hpp" +#include "../../test_classes.hpp" + +using namespace boost::openmethod; + +BOOST_OPENMETHOD_INSTANTIATE_REGISTRY(boost::openmethod::default_registry); + +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog); + +auto lib_registry_state_id() -> const void* { + return default_registry::id(); +} + +auto lib_poke(virtual_ptr animal) -> const char* { + return Zoo::poke(animal); +} + +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/implicit_shared_libraries/member_overrider/lib.hpp b/test/implicit_shared_libraries/member_overrider/lib.hpp new file mode 100644 index 00000000..b6d9e19f --- /dev/null +++ b/test/implicit_shared_libraries/member_overrider/lib.hpp @@ -0,0 +1,52 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#ifndef BOOST_OPENMETHOD_TEST_ISL_MEMBER_OVERRIDER_LIB_HPP +#define BOOST_OPENMETHOD_TEST_ISL_MEMBER_OVERRIDER_LIB_HPP + +#include +#include + +#if defined(OWNS_REGISTRY_STATE) +BOOST_OPENMETHOD_EXPORT_REGISTRY(boost::openmethod::default_registry); +#else +BOOST_OPENMETHOD_IMPORT_REGISTRY(boost::openmethod::default_registry); +#endif + +struct BOOST_SYMBOL_VISIBLE Animal { + virtual ~Animal() = default; +}; + +struct BOOST_SYMBOL_VISIBLE Dog : Animal {}; + +// A member method and a member overrider, both in a header, so both modules +// declare them and both register. Everything the _MEM macros emit is a class +// member, so the two modules must agree on it exactly; and the overrider, +// being one overrider rather than two, must end up registered once. +struct BOOST_SYMBOL_VISIBLE Zoo { + BOOST_OPENMETHOD_MEM( + poke, (boost::openmethod::virtual_ptr), const char*); +}; + +class BOOST_SYMBOL_VISIBLE Keeper { + BOOST_OPENMETHOD_OVERRIDE_MEM( + Zoo::poke, (boost::openmethod::virtual_ptr), const char*) { + return "woof"; + } +}; + +#if defined(LIB_SOURCE) +#define LIB_API BOOST_SYMBOL_EXPORT +#else +#define LIB_API BOOST_SYMBOL_IMPORT +#endif + +LIB_API auto lib_registry_state_id() -> const void*; + +// Dispatch through the member method, performed inside the library. +LIB_API auto lib_poke(boost::openmethod::virtual_ptr animal) + -> const char*; + +#endif diff --git a/test/implicit_shared_libraries/member_overrider/main.cpp b/test/implicit_shared_libraries/member_overrider/main.cpp new file mode 100644 index 00000000..c2303675 --- /dev/null +++ b/test/implicit_shared_libraries/member_overrider/main.cpp @@ -0,0 +1,37 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#define BOOST_TEST_MODULE implicit_shared_libraries_member_overrider + +#include + +#include "lib.hpp" +#include "../../test_classes.hpp" + +#include + +#include + +using namespace boost::openmethod; + +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog); + +BOOST_AUTO_TEST_CASE(member_overrider_across_modules) { + BOOST_TEST(lib_registry_state_id() == default_registry::id()); + + auto report = initialize().report; + + // Keeper's overrider is declared in a header both modules include, so each + // registered its own copy. They are copies of one overrider, not two + // overriders, and must be consolidated: keeping both would make the Dog + // cell ambiguous, since neither copy is more specific than the other. + BOOST_TEST(report.ambiguous == 0u); + + Dog dog; + BOOST_TEST(std::string(Zoo::poke(dog)) == "woof"); + BOOST_TEST(std::string(lib_poke(dog)) == "woof"); +} + +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_member_method.cpp b/test/test_member_method.cpp new file mode 100644 index 00000000..d9ba8e54 --- /dev/null +++ b/test/test_member_method.cpp @@ -0,0 +1,256 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +#include + +#include +#include + +#include "test_util.hpp" + +#define BOOST_TEST_MODULE member_method +#include + +using namespace boost::openmethod; + +// ---------------------------------------------------------------------------- +// BOOST_OPENMETHOD_MEM: a member method, overloaded, and BOOST_OPENMETHOD_TYPE_MEM. + +namespace member_method { + +struct Animal { + virtual ~Animal() = default; +}; + +struct Dog : Animal {}; +struct Cat : Animal {}; + +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat); + +struct Zoo { + BOOST_OPENMETHOD_MEM(poke, (virtual_ptr), std::string); + BOOST_OPENMETHOD_MEM(poke, (virtual_ptr, int times), std::string); +}; + +static_assert(!std::is_same_v< + BOOST_OPENMETHOD_TYPE_MEM( + Zoo::poke, (virtual_ptr), std::string), + BOOST_OPENMETHOD_TYPE_MEM( + Zoo::poke, (virtual_ptr, int), std::string)>); + +// Overriders of the member method, in a class of their own - free-standing +// static member functions, no receiver, no friend needed here since nothing +// private is touched. Both overloads of Zoo::poke are overridden. +class ZooKeeper { + BOOST_OPENMETHOD_OVERRIDE_MEM( + Zoo::poke, (virtual_ptr d), std::string) { + (void)d; + return "one bark"; + } + BOOST_OPENMETHOD_OVERRIDE_MEM( + Zoo::poke, (virtual_ptr d, int n), std::string) { + (void)d; + std::string result; + for (int i = 0; i < n; ++i) { + result += "bark "; + } + return result; + } + + public: + // Public, so the explicit-lookup test below can find it. + BOOST_OPENMETHOD_OVERRIDE_MEM(Zoo::poke, (virtual_ptr), std::string) { + return "one meow"; + } +}; + +using poke_cat_key = BOOST_OPENMETHOD_OVERRIDER_MEM( + ZooKeeper, Zoo::poke, (virtual_ptr), std::string); + +} // namespace member_method + +BOOST_AUTO_TEST_CASE(member_method_call_and_overload) { + initialize(); + + using namespace member_method; + + Dog snoopy; + Cat felix; + + BOOST_TEST(Zoo::poke(snoopy) == "one bark"); + BOOST_TEST(Zoo::poke(snoopy, 3) == "bark bark bark "); + BOOST_TEST(Zoo::poke(felix) == "one meow"); + + // explicit call, no dispatch + BOOST_TEST(poke_cat_key::fn(virtual_ptr(felix)) == "one meow"); + BOOST_TEST(!poke_cat_key::method_type::has_next()); +} + +// ---------------------------------------------------------------------------- +// Member overriders targeting a FREE method, with private access and no +// friend - the motivating case, mirroring the privacy.adoc Payroll example. +// Also exercises the DECLARE/DEFINE split and next<>/has_next<> through the +// core API from inside a _MEM body (self-referencing key). + +namespace member_overrider { + +struct Employee { + virtual ~Employee() = default; +}; + +struct Salesman : Employee { + double sales = 0.0; +}; + +BOOST_OPENMETHOD_TEST_CLASSES(Employee, Salesman); + +// Only a reference to it appears in the method's parameter list, so the +// forward declaration is enough - Payroll is not an Employee, and is not +// dispatched on. +class Payroll; + +BOOST_OPENMETHOD(pay, (Payroll & payroll, virtual_ptr), double); + +class Payroll { + public: + double balance() const { + return balance_; + } + + private: + double balance_ = 1'000'000.0; + + void update_balance(double amount) { + // Private, reachable only because the overriders below are members. + balance_ += amount; + } + + BOOST_OPENMETHOD_OVERRIDE_MEM( + pay, (Payroll & payroll, virtual_ptr), double) { + payroll.update_balance(-5000.0); + return 5000.0; + } + + BOOST_OPENMETHOD_DECLARE_OVERRIDER_MEM( + pay, (Payroll & payroll, virtual_ptr emp), double); +}; + +BOOST_OPENMETHOD_DEFINE_OVERRIDER_MEM( + Payroll, pay, (Payroll & payroll, virtual_ptr emp), + double) { + // Self-referencing key: names *this* overrider, not the one it calls. + using self_key = BOOST_OPENMETHOD_OVERRIDER_MEM( + Payroll, pay, (Payroll&, virtual_ptr), double); + double base = self_key::method_type::next(payroll, emp); + double commission = emp->sales * 0.05; + payroll.update_balance(-commission); + return base + commission; +} + +} // namespace member_overrider + +BOOST_AUTO_TEST_CASE(member_overrider_private_access_and_next) { + initialize(); + + using namespace member_overrider; + + Payroll payroll; + Employee bill; + Salesman bob; + bob.sales = 100'000.0; + + BOOST_TEST(pay(payroll, bill) == 5000.0); + BOOST_TEST(pay(payroll, bob) == 10000.0); + BOOST_TEST(payroll.balance() == 985000.0); +} + +// ---------------------------------------------------------------------------- +// A return type containing a comma. The preprocessor hands it over as several +// macro arguments; they reassemble inside va_args_no_registry's template +// argument list, which is exactly what tells a comma-bearing return type apart +// from a trailing registry. Before va_args_no_registry the free macros used +// mp_back for this, and the _MEM ones pasted __VA_ARGS__ in raw. + +namespace comma_return { + +struct Animal { + virtual ~Animal() = default; +}; + +struct Dog : Animal {}; +struct Cat : Animal {}; + +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat); + +struct Zoo { + BOOST_OPENMETHOD_MEM(weigh, (virtual_ptr), std::pair); +}; + +class Scale { + // In-class body. + BOOST_OPENMETHOD_OVERRIDE_MEM( + Zoo::weigh, (virtual_ptr), std::pair) { + return {1, 2}; + } + + public: + // DECLARE/DEFINE split, defined at namespace scope below. + BOOST_OPENMETHOD_DECLARE_OVERRIDER_MEM( + Zoo::weigh, (virtual_ptr), std::pair); +}; + +BOOST_OPENMETHOD_DEFINE_OVERRIDER_MEM( + Scale, Zoo::weigh, (virtual_ptr), std::pair) { + return {3, 4}; +} + +// Naming the method, and naming an overrider, both with a comma in the return +// type - and they agree on the method. +using weigh_method = BOOST_OPENMETHOD_TYPE_MEM( + Zoo::weigh, (virtual_ptr), std::pair); +using weigh_cat = BOOST_OPENMETHOD_OVERRIDER_MEM( + Scale, Zoo::weigh, (virtual_ptr), std::pair); +static_assert(std::is_same_v); + +} // namespace comma_return + +// A dependent return type: the macros must not require `typename` from the +// caller. va_args_no_registry's return_type is a nested typedef, so the macro +// supplies it - 459eb0e briefly did not, and this shape stopped compiling. +namespace dependent_return { + +using namespace comma_return; + +template +struct Probe { + using key = BOOST_OPENMETHOD_OVERRIDER_MEM( + Scale, Zoo::weigh, (virtual_ptr), R); +}; + +static_assert(std::is_same_v< + Probe>::key, + BOOST_OPENMETHOD_OVERRIDER_MEM( + Scale, Zoo::weigh, (virtual_ptr), std::pair)>); + +} // namespace dependent_return + +BOOST_AUTO_TEST_CASE(member_method_comma_return_type) { + initialize(); + + using namespace comma_return; + + Dog snoopy; + Cat felix; + + BOOST_TEST((Zoo::weigh(snoopy) == std::pair{1, 2})); + BOOST_TEST((Zoo::weigh(felix) == std::pair{3, 4})); + + // explicit call through the overrider key, no dispatch + BOOST_TEST( + (weigh_cat::fn(virtual_ptr(felix)) == std::pair{3, 4})); +} + +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_runtime_errors_inline_overrider_identity.cpp b/test/test_runtime_errors_inline_overrider_identity.cpp new file mode 100644 index 00000000..9f13079c --- /dev/null +++ b/test/test_runtime_errors_inline_overrider_identity.cpp @@ -0,0 +1,59 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include "test_capture_errors.hpp" + +#include + +#include "test_classes.hpp" + +#define BOOST_TEST_MODULE runtime_errors_inline_overrider_identity +#include + +using namespace boost::openmethod; + +using capture = capture_errors; + +struct Animal { + virtual ~Animal() { + } +}; + +struct Dog : Animal {}; + +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog); + +BOOST_OPENMETHOD(poke, (virtual_ptr), const char*); + +// Two genuinely different overriders of one method, sharing a signature, each +// declared `inline`. augment_methods() merges the copies of an inline +// overrider that several modules registered, and these are the shape that +// merge keys on: same function type, same virtual parameter types, both +// flagged inline_. They are not the same overrider, so they must stay +// ambiguous - overrider_info::identity is what tells the two cases apart. +// Before it existed, this program printed "a" and reported no error. +namespace a { +BOOST_OPENMETHOD_INLINE_OVERRIDE(poke, (virtual_ptr), const char*) { + return "a"; +} +} // namespace a + +namespace b { +BOOST_OPENMETHOD_INLINE_OVERRIDE(poke, (virtual_ptr), const char*) { + return "b"; +} +} // namespace b + +BOOST_AUTO_TEST_CASE(distinct_inline_overriders_are_ambiguous) { + auto report = initialize().report; + BOOST_TEST(report.ambiguous == 1u); + + capture capture; + Dog dog; + BOOST_CHECK_THROW(poke(dog), ambiguous_call); + BOOST_TEST(capture().find("ambiguous") != std::string::npos); +} + +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_runtime_errors_member_overrider_identity.cpp b/test/test_runtime_errors_member_overrider_identity.cpp new file mode 100644 index 00000000..96d2c822 --- /dev/null +++ b/test/test_runtime_errors_member_overrider_identity.cpp @@ -0,0 +1,57 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include "test_capture_errors.hpp" + +#include + +#include "test_classes.hpp" + +#define BOOST_TEST_MODULE runtime_errors_member_overrider_identity +#include + +using namespace boost::openmethod; + +using capture = capture_errors; + +struct Animal { + virtual ~Animal() { + } +}; + +struct Dog : Animal {}; + +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog); + +BOOST_OPENMETHOD(poke, (virtual_ptr), const char*); + +// The same shape as the namespace-scope test beside this one, but reached the +// way a member overrider makes easy: a _MEM overrider's body is in a class, so +// it is implicitly inline and registers as inline_. Two classes may each add +// an overrider of one method with one signature - which the free macros cannot +// express in a single translation unit - and the two are distinct. +class VetA { + BOOST_OPENMETHOD_OVERRIDE_MEM(poke, (virtual_ptr), const char*) { + return "A"; + } +}; + +class VetB { + BOOST_OPENMETHOD_OVERRIDE_MEM(poke, (virtual_ptr), const char*) { + return "B"; + } +}; + +BOOST_AUTO_TEST_CASE(distinct_member_overriders_are_ambiguous) { + auto report = initialize().report; + BOOST_TEST(report.ambiguous == 1u); + + capture capture; + Dog dog; + BOOST_CHECK_THROW(poke(dog), ambiguous_call); + BOOST_TEST(capture().find("ambiguous") != std::string::npos); +} + +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES();