From 0ed69780175c274f5a9a83d968ac82e990afd30d Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 19 Sep 2026 12:30:49 -0400 Subject: [PATCH 01/13] overriders: BOOST_OPENMETHOD_MEM family for methods/overriders as static members (#53) Alternative design to #120: six macros mirroring BOOST_OPENMETHOD and its overrider macros, but declaring static member functions instead of free ones. No implicit `this`, no receiver binding - dispatch is still entirely by the method's own virtual parameters. - BOOST_OPENMETHOD_MEM declares a method as a static member function of the class it's used in, overloadable exactly like a free method. Its tag is generated per-expansion rather than ID-derived, which is what makes the overload possible. - BOOST_OPENMETHOD_TYPE_MEM names a member method's core `method<>` type, since BOOST_OPENMETHOD_TYPE can't (it reconstructs the tag from ID alone). - BOOST_OPENMETHOD_OVERRIDE_MEM / _DECLARE_OVERRIDER_MEM / _DEFINE_OVERRIDER_MEM add an overrider, as a static member function, to either a free method or a member method (qualified as Class::method). Being a member gives it the same access to its class's private state as any other member, with no `friend` declaration. - BOOST_OPENMETHOD_OVERRIDER_MEM finds a member overrider's key (`fn`, `method_type`) from outside, for explicit calls and for `next`/`has_next` via the core API - not available by name inside a _MEM body the way they are in a free one's. No core.hpp changes: each overrider is registered through a per-overrider "key" struct holding a BOOST_FORCEINLINE trampoline, so override_aux/thunk/ validate_overrider_parameter all see an ordinary function pointer, unchanged. Verified on gcc, clang and MSVC, including private access, overloaded member methods, and next<> from a DECLARE/DEFINE-split body (self-referencing, easy to get backwards - documented prominently). Known limitation: a _MEM overrider's body and key-accessor are each named once, overloaded purely on (return type, parameters) - never on which method they override, since a qualified ID can't be pasted into a new declaration. At most one overrider of a given exact signature per class, regardless of method. doc/modules/ROOT/pages/friends.adoc is retitled "Members and Friends" in the nav and gains a _MEM tutorial before the existing `friend` content, which it supersedes for classes under the caller's control. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP --- doc/modules/ROOT/examples/rolex/8/main.cpp | 116 +++++++++ doc/modules/ROOT/nav.adoc | 2 +- doc/modules/ROOT/pages/friends.adoc | 56 +++++ doc/modules/ROOT/pages/ref_macros.adoc | 12 + doc/modules/ROOT/snippets/member.cpp | 66 +++++ include/boost/openmethod/macros.hpp | 232 +++++++++++++++++- test/compile_fail_member_method_private.cpp | 37 +++ ...il_member_overrider_parameter_mismatch.cpp | 44 ++++ ...l_member_overrider_signature_collision.cpp | 42 ++++ test/test_member_method.cpp | 165 +++++++++++++ 10 files changed, 770 insertions(+), 2 deletions(-) create mode 100644 doc/modules/ROOT/examples/rolex/8/main.cpp create mode 100644 doc/modules/ROOT/snippets/member.cpp create mode 100644 test/compile_fail_member_method_private.cpp create mode 100644 test/compile_fail_member_overrider_parameter_mismatch.cpp create mode 100644 test/compile_fail_member_overrider_signature_collision.cpp create mode 100644 test/test_member_method.cpp 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..8bf1ed21 --- /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[] + +struct Employee { + virtual ~Employee() = default; +}; + +struct Salesman : Employee { + double sales = 0.0; +}; + +// tag::pay[] +BOOST_OPENMETHOD( + pay, (Employee & payroll, boost::openmethod::virtual_ptr), + double); +// end::pay[] + +// tag::payroll[] +class Payroll : public Employee { + 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, + (Employee & payroll, boost::openmethod::virtual_ptr), + double) { + double amount = 5000.0; + static_cast(payroll).update_balance(-amount); + return amount; + } + + BOOST_OPENMETHOD_OVERRIDE_MEM( + pay, + (Employee & payroll, + boost::openmethod::virtual_ptr emp), + double) { + using self = BOOST_OPENMETHOD_OVERRIDER_MEM( + Payroll, pay, + (Employee&, boost::openmethod::virtual_ptr), + double); + double base = self::method_type::next(payroll, emp); + double commission = emp->sales * 0.05; + static_cast(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..b9b73a38 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:friends.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/friends.adoc b/doc/modules/ROOT/pages/friends.adoc index dbe82d44..3bd6d0cb 100644 --- a/doc/modules/ROOT/pages/friends.adoc +++ b/doc/modules/ROOT/pages/friends.adoc @@ -1,5 +1,61 @@ :example: ../examples/rolex +[#members] + +A method may itself be a `static` member function of a class, declared with +`BOOST_OPENMETHOD_MEM` instead of `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 +`BOOST_OPENMETHOD_OVERRIDE_MEM` instead of `BOOST_OPENMETHOD_OVERRIDE`: + +[source,c++] +---- +include::{example}/8/main.cpp[tag=zookeeper] +---- + +`ID` names the method being overridden - here `Zoo::poke` - and may equally +name a free method declared with `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 `BOOST_OPENMETHOD_OVERRIDER_MEM`, naming +*itself* (its own `(Class, ID, PARAMETERS, RETURN)`), 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 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/macros.hpp b/include/boost/openmethod/macros.hpp index 4e2401be..a36a486e 100644 --- a/include/boost/openmethod/macros.hpp +++ b/include/boost/openmethod/macros.hpp @@ -528,7 +528,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:friends.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 +583,236 @@ 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:friends.adoc) +#define BOOST_OPENMETHOD_MEM(ID, PARAMETERS, ...) \ + BOOST_OPENMETHOD_DETAIL_MEM( \ + BOOST_OPENMETHOD_GENSYM, BOOST_OPENMETHOD_GENSYM, 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. +//! +//! @see [Members and Friends](xref:ROOT:friends.adoc) +#define BOOST_OPENMETHOD_TYPE_MEM(ID, PARAMETERS, ...) \ + decltype(BOOST_OPENMETHOD_ID(ID)( \ + static_cast<::boost::openmethod::detail::va_args< \ + __VA_ARGS__>::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. +#define BOOST_OPENMETHOD_DETAIL_OVERRIDE_MEM( \ + 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_FORCEINLINE auto trampoline(ForwarderParameters... args) \ + -> __VA_ARGS__ { \ + 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 inline KEY::method_type::REGISTRAR \ + BOOST_OPENMETHOD_GENSYM; \ + 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. +//! +//! @see [Members and Friends](xref:ROOT:friends.adoc) +#define BOOST_OPENMETHOD_OVERRIDE_MEM(ID, PARAMETERS, ...) \ + BOOST_OPENMETHOD_DETAIL_OVERRIDE_MEM( \ + BOOST_OPENMETHOD_GENSYM, 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. +//! +//! @see [Members and Friends](xref:ROOT:friends.adoc) +#define BOOST_OPENMETHOD_DECLARE_OVERRIDER_MEM(ID, PARAMETERS, ...) \ + BOOST_OPENMETHOD_DETAIL_OVERRIDE_MEM( \ + BOOST_OPENMETHOD_GENSYM, 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. +//! +//! @see [Members and Friends](xref:ROOT:friends.adoc) +#define BOOST_OPENMETHOD_DEFINE_OVERRIDER_MEM(CLASS, ID, PARAMETERS, ...) \ + auto CLASS::boost_openmethod_overrider_body PARAMETERS \ + ->boost::mp11::mp_back> + +//! 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. +//! +//! @see [Members and Friends](xref:ROOT:friends.adoc) +#define BOOST_OPENMETHOD_OVERRIDER_MEM(CLASS, ID, PARAMETERS, ...) \ + decltype(CLASS::boost_openmethod_overrider_key( \ + static_cast<__VA_ARGS__(*) PARAMETERS>(nullptr))) + //! Register classes. //! //! Registers classes in a registry. 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_signature_collision.cpp b/test/compile_fail_member_overrider_signature_collision.cpp new file mode 100644 index 00000000..012ed921 --- /dev/null +++ b/test/compile_fail_member_overrider_signature_collision.cpp @@ -0,0 +1,42 @@ +// 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) + +// The two compilers disagree on the wording for this one: gcc says "cannot +// be overloaded with", clang says "class member cannot be redeclared" - no +// common substring, hence the `.*`. +// expected-error: .* + +#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/test_member_method.cpp b/test/test_member_method.cpp new file mode 100644 index 00000000..cdbc3c24 --- /dev/null +++ b/test/test_member_method.cpp @@ -0,0 +1,165 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include + +#include +#include + +#include "test_util.hpp" + +#define BOOST_TEST_MODULE member_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 friends.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); + +BOOST_OPENMETHOD( + pay, (Employee & payroll, virtual_ptr), double); + +class Payroll : public Employee { + 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, (Employee & payroll, virtual_ptr), double) { + static_cast(payroll).update_balance(-5000.0); + return 5000.0; + } + + BOOST_OPENMETHOD_DECLARE_OVERRIDER_MEM( + pay, (Employee & payroll, virtual_ptr emp), double); +}; + +BOOST_OPENMETHOD_DEFINE_OVERRIDER_MEM( + Payroll, pay, (Employee & 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, (Employee&, virtual_ptr), double); + double base = self_key::method_type::next(payroll, emp); + double commission = emp->sales * 0.05; + static_cast(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); +} + +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); From 3bea0a628354ca4a3c2c0bc1162c5dbe0ba5c429 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 20 Sep 2026 12:29:54 -0400 Subject: [PATCH 02/13] doc: link the macro names on the Members and Friends page to the reference The page named the macros in plain code spans while every other page links them, so a reader had no way from the tutorial to the reference page that describes the arguments. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP --- doc/modules/ROOT/pages/friends.adoc | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/doc/modules/ROOT/pages/friends.adoc b/doc/modules/ROOT/pages/friends.adoc index 3bd6d0cb..49c741d0 100644 --- a/doc/modules/ROOT/pages/friends.adoc +++ b/doc/modules/ROOT/pages/friends.adoc @@ -3,7 +3,8 @@ [#members] A method may itself be a `static` member function of a class, declared with -`BOOST_OPENMETHOD_MEM` instead of `BOOST_OPENMETHOD`: +xref:reference:BOOST_OPENMETHOD_MEM.adoc[BOOST_OPENMETHOD_MEM] instead of +xref:reference:BOOST_OPENMETHOD.adoc[BOOST_OPENMETHOD]: [source,c++] ---- @@ -18,7 +19,8 @@ scope. An overrider, too, may be a `static` member function - of any class, not necessarily the one the method belongs to - added with -`BOOST_OPENMETHOD_OVERRIDE_MEM` instead of `BOOST_OPENMETHOD_OVERRIDE`: +xref:reference:BOOST_OPENMETHOD_OVERRIDE_MEM.adoc[BOOST_OPENMETHOD_OVERRIDE_MEM] +instead of xref:reference:BOOST_OPENMETHOD_OVERRIDE.adoc[BOOST_OPENMETHOD_OVERRIDE]: [source,c++] ---- @@ -26,7 +28,8 @@ include::{example}/8/main.cpp[tag=zookeeper] ---- `ID` names the method being overridden - here `Zoo::poke` - and may equally -name a free method declared with `BOOST_OPENMETHOD`. +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 @@ -47,9 +50,10 @@ include::{example}/8/main.cpp[tag=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 `BOOST_OPENMETHOD_OVERRIDER_MEM`, naming -*itself* (its own `(Class, ID, PARAMETERS, RETURN)`), not the overrider it -calls. +through the core API instead, via +xref:reference:BOOST_OPENMETHOD_OVERRIDER_MEM.adoc[BOOST_OPENMETHOD_OVERRIDER_MEM], +naming *itself* (its own `(Class, ID, PARAMETERS, RETURN)`), 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 @@ -74,12 +78,12 @@ we pass the payroll object to the `pay` method: 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: +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++] ---- From 5b2e7158bb0e624ac8bfaf3a0fe1db98a1402a20 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 20 Sep 2026 12:30:05 -0400 Subject: [PATCH 03/13] doc, test: pay a Payroll, not an Employee The `pay` example declared its first parameter `Employee&` and made Payroll derive from Employee so that each overrider could cast it back - which reads as if a payroll were a kind of employee, and put a `static_cast` in front of every call to the private member the example exists to demonstrate. Declare the parameter `Payroll&` instead, over a forward declaration, exactly as the `friend` example further down the same page already does: only a reference appears in the parameter list, and Payroll is not dispatched on. The inheritance and all three casts go away, and the page's claim that the overriders "call it as an ordinary same-class private call" becomes literally true. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP --- doc/modules/ROOT/examples/rolex/8/main.cpp | 18 +++++++++--------- test/test_member_method.cpp | 22 +++++++++++++--------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/doc/modules/ROOT/examples/rolex/8/main.cpp b/doc/modules/ROOT/examples/rolex/8/main.cpp index 8bf1ed21..0412cc0c 100644 --- a/doc/modules/ROOT/examples/rolex/8/main.cpp +++ b/doc/modules/ROOT/examples/rolex/8/main.cpp @@ -37,6 +37,8 @@ class ZooKeeper { }; // end::zookeeper[] +class Payroll; + struct Employee { virtual ~Employee() = default; }; @@ -47,12 +49,12 @@ struct Salesman : Employee { // tag::pay[] BOOST_OPENMETHOD( - pay, (Employee & payroll, boost::openmethod::virtual_ptr), + pay, (Payroll & payroll, boost::openmethod::virtual_ptr), double); // end::pay[] // tag::payroll[] -class Payroll : public Employee { +class Payroll { public: double balance() const { return balance_; @@ -68,25 +70,23 @@ class Payroll : public Employee { BOOST_OPENMETHOD_OVERRIDE_MEM( pay, - (Employee & payroll, boost::openmethod::virtual_ptr), + (Payroll & payroll, boost::openmethod::virtual_ptr), double) { double amount = 5000.0; - static_cast(payroll).update_balance(-amount); + payroll.update_balance(-amount); return amount; } BOOST_OPENMETHOD_OVERRIDE_MEM( pay, - (Employee & payroll, - boost::openmethod::virtual_ptr emp), + (Payroll & payroll, boost::openmethod::virtual_ptr emp), double) { using self = BOOST_OPENMETHOD_OVERRIDER_MEM( Payroll, pay, - (Employee&, boost::openmethod::virtual_ptr), - double); + (Payroll&, boost::openmethod::virtual_ptr), double); double base = self::method_type::next(payroll, emp); double commission = emp->sales * 0.05; - static_cast(payroll).update_balance(-commission); + payroll.update_balance(-commission); return base + commission; } }; diff --git a/test/test_member_method.cpp b/test/test_member_method.cpp index cdbc3c24..666b0662 100644 --- a/test/test_member_method.cpp +++ b/test/test_member_method.cpp @@ -106,10 +106,14 @@ struct Salesman : Employee { BOOST_OPENMETHOD_TEST_CLASSES(Employee, Salesman); -BOOST_OPENMETHOD( - pay, (Employee & payroll, virtual_ptr), double); +// 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; -class Payroll : public Employee { +BOOST_OPENMETHOD(pay, (Payroll & payroll, virtual_ptr), double); + +class Payroll { public: double balance() const { return balance_; @@ -124,24 +128,24 @@ class Payroll : public Employee { } BOOST_OPENMETHOD_OVERRIDE_MEM( - pay, (Employee & payroll, virtual_ptr), double) { - static_cast(payroll).update_balance(-5000.0); + pay, (Payroll & payroll, virtual_ptr), double) { + payroll.update_balance(-5000.0); return 5000.0; } BOOST_OPENMETHOD_DECLARE_OVERRIDER_MEM( - pay, (Employee & payroll, virtual_ptr emp), double); + pay, (Payroll & payroll, virtual_ptr emp), double); }; BOOST_OPENMETHOD_DEFINE_OVERRIDER_MEM( - Payroll, pay, (Employee & payroll, virtual_ptr emp), + 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, (Employee&, virtual_ptr), double); + Payroll, pay, (Payroll&, virtual_ptr), double); double base = self_key::method_type::next(payroll, emp); double commission = emp->sales * 0.05; - static_cast(payroll).update_balance(-commission); + payroll.update_balance(-commission); return base + commission; } From fd2a53983c345cb20f6c9d9fcdd9c7f34b28f8d4 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 20 Sep 2026 12:47:00 -0400 Subject: [PATCH 04/13] doc: rename the friends page to privacy.adoc The page now covers both ways an overrider reaches a class's private state - being a member of it, and `friend` - so the file name names the subject rather than one of the two answers. The nav label and the `@see` link text are unchanged; only the path moves. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP --- doc/modules/ROOT/nav.adoc | 2 +- .../ROOT/pages/{friends.adoc => privacy.adoc} | 0 include/boost/openmethod/macros.hpp | 14 +++++++------- 3 files changed, 8 insertions(+), 8 deletions(-) rename doc/modules/ROOT/pages/{friends.adoc => privacy.adoc} (100%) diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index b9b73a38..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[Members and 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/friends.adoc b/doc/modules/ROOT/pages/privacy.adoc similarity index 100% rename from doc/modules/ROOT/pages/friends.adoc rename to doc/modules/ROOT/pages/privacy.adoc diff --git a/include/boost/openmethod/macros.hpp b/include/boost/openmethod/macros.hpp index a36a486e..b635f83b 100644 --- a/include/boost/openmethod/macros.hpp +++ b/include/boost/openmethod/macros.hpp @@ -528,7 +528,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 [Members and 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__) @@ -643,7 +643,7 @@ inline constexpr bool method_not_found = false; //! @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:friends.adoc) +//! @see [Members and Friends](xref:ROOT:privacy.adoc) #define BOOST_OPENMETHOD_MEM(ID, PARAMETERS, ...) \ BOOST_OPENMETHOD_DETAIL_MEM( \ BOOST_OPENMETHOD_GENSYM, BOOST_OPENMETHOD_GENSYM, ID, PARAMETERS, \ @@ -665,7 +665,7 @@ inline constexpr bool method_not_found = false; //! @param PARAMETERS The method's parameter list, in parentheses. //! @param ... The method's return type. //! -//! @see [Members and Friends](xref:ROOT:friends.adoc) +//! @see [Members and Friends](xref:ROOT:privacy.adoc) #define BOOST_OPENMETHOD_TYPE_MEM(ID, PARAMETERS, ...) \ decltype(BOOST_OPENMETHOD_ID(ID)( \ static_cast<::boost::openmethod::detail::va_args< \ @@ -741,7 +741,7 @@ inline constexpr bool method_not_found = false; //! @param PARAMETERS The overrider's parameter list, in parentheses. //! @param ... The overrider's return type. //! -//! @see [Members and Friends](xref:ROOT:friends.adoc) +//! @see [Members and Friends](xref:ROOT:privacy.adoc) #define BOOST_OPENMETHOD_OVERRIDE_MEM(ID, PARAMETERS, ...) \ BOOST_OPENMETHOD_DETAIL_OVERRIDE_MEM( \ BOOST_OPENMETHOD_GENSYM, inline_override, ID, PARAMETERS, __VA_ARGS__) @@ -757,7 +757,7 @@ inline constexpr bool method_not_found = false; //! @param PARAMETERS The overrider's parameter list, in parentheses. //! @param ... The overrider's return type. //! -//! @see [Members and Friends](xref:ROOT:friends.adoc) +//! @see [Members and Friends](xref:ROOT:privacy.adoc) #define BOOST_OPENMETHOD_DECLARE_OVERRIDER_MEM(ID, PARAMETERS, ...) \ BOOST_OPENMETHOD_DETAIL_OVERRIDE_MEM( \ BOOST_OPENMETHOD_GENSYM, override, ID, PARAMETERS, __VA_ARGS__) @@ -773,7 +773,7 @@ inline constexpr bool method_not_found = false; //! @param PARAMETERS The overrider's parameter list, in parentheses. //! @param ... The overrider's return type. //! -//! @see [Members and Friends](xref:ROOT:friends.adoc) +//! @see [Members and Friends](xref:ROOT:privacy.adoc) #define BOOST_OPENMETHOD_DEFINE_OVERRIDER_MEM(CLASS, ID, PARAMETERS, ...) \ auto CLASS::boost_openmethod_overrider_body PARAMETERS \ ->boost::mp11::mp_back> @@ -808,7 +808,7 @@ inline constexpr bool method_not_found = false; //! @param PARAMETERS The overrider's parameter list, in parentheses. //! @param ... The overrider's return type. //! -//! @see [Members and Friends](xref:ROOT:friends.adoc) +//! @see [Members and Friends](xref:ROOT:privacy.adoc) #define BOOST_OPENMETHOD_OVERRIDER_MEM(CLASS, ID, PARAMETERS, ...) \ decltype(CLASS::boost_openmethod_overrider_key( \ static_cast<__VA_ARGS__(*) PARAMETERS>(nullptr))) From 7662985d85ec1288157f1bccef51d06452d691be Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 20 Sep 2026 12:54:36 -0400 Subject: [PATCH 05/13] doc: name the overrider macro's arguments by role, not by formal name The tutorial referred to `ID`, and to `(Class, ID, PARAMETERS, RETURN)`, which are the reference pages' formal parameter names and are never introduced on the tutorial page itself. Describe the arguments by what they are instead. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP --- doc/modules/ROOT/pages/privacy.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/modules/ROOT/pages/privacy.adoc b/doc/modules/ROOT/pages/privacy.adoc index 49c741d0..8b96ec16 100644 --- a/doc/modules/ROOT/pages/privacy.adoc +++ b/doc/modules/ROOT/pages/privacy.adoc @@ -27,8 +27,8 @@ instead of xref:reference:BOOST_OPENMETHOD_OVERRIDE.adoc[BOOST_OPENMETHOD_OVERRI include::{example}/8/main.cpp[tag=zookeeper] ---- -`ID` names the method being overridden - here `Zoo::poke` - and may equally -name a free method declared with +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 @@ -52,8 +52,8 @@ 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], -naming *itself* (its own `(Class, ID, PARAMETERS, RETURN)`), not the overrider -it calls. +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 From 6cb10b40cf0f2639026e22a2777089f71f378f38 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 20 Sep 2026 15:08:26 -0400 Subject: [PATCH 06/13] macros: reject a registry argument in the _MEM overrider and lookup macros BOOST_OPENMETHOD and BOOST_OPENMETHOD_MEM take an optional registry after the return type. The macros that name an existing member method, or that declare an overrider, cannot use one: a member method's registry is fixed by the declaration that created it, and an overrider takes the registry of the method it overrides, which LOCATE_METHOD finds through a guide lookup that enable_guide_ignoring_registry makes registry-agnostic. Passing one anyway produced two different failures, neither legible. BOOST_OPENMETHOD_TYPE_MEM accepted it and silently ignored it, because va_args<...>::return_type takes the first argument and drops the rest. The four overrider macros pasted __VA_ARGS__ raw into a function pointer type, which gave three cascading parse errors on the user's own line with nothing to suggest the cause. **This makes a previously tolerated input an error**: TYPE_MEM with a trailing registry no longer compiles. There are no call sites. Route all five through a new detail::va_args_no_registry, whose variadic specialization is a static_assert naming the reason. Its return_type lives in a base class: a failed static_assert marks the record invalid on clang, so a member declared alongside it is not found and the cascade returns. With the base, gcc reports exactly one error and clang leads with the message. A return type containing a top-level comma still arrives as several macro arguments and reassembles inside the template argument list, which is what tells the two cases apart - the same discrimination va_args already relies on, and the reason DEFINE_OVERRIDER_MEM can stop using mp_back for it. DETAIL_OVERRIDE_MEM forwards the return type to a new _AUX helper, which is variadic rather than taking a named RET: a comma-bearing return type would otherwise be split into several arguments before _AUX could receive it, and the expansion would fail on arity. Tests: a comma-bearing return type through all three _MEM overrider shapes plus TYPE_MEM and OVERRIDER_MEM, which nothing covered before and which va_args_no_registry is now solely responsible for; and a compile-fail test for the rejected registry. 204/204. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP --- include/boost/openmethod/macros.hpp | 63 ++++++++++++++--- ...ail_member_overrider_registry_argument.cpp | 39 +++++++++++ test/test_member_method.cpp | 67 +++++++++++++++++++ 3 files changed, 160 insertions(+), 9 deletions(-) create mode 100644 test/compile_fail_member_overrider_registry_argument.cpp diff --git a/include/boost/openmethod/macros.hpp b/include/boost/openmethod/macros.hpp index b635f83b..cd96a5ca 100644 --- a/include/boost/openmethod/macros.hpp +++ b/include/boost/openmethod/macros.hpp @@ -64,6 +64,38 @@ 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"); +}; + template inline constexpr bool method_not_found = false; @@ -663,12 +695,13 @@ inline constexpr bool method_not_found = false; //! //! @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. +//! @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<::boost::openmethod::detail::va_args< \ + static_cast<::boost::openmethod::detail::va_args_no_registry< \ __VA_ARGS__>::return_type(*) PARAMETERS>(nullptr))) // The overrider's body cannot be named after ID: ID may be qualified (e.g. @@ -693,6 +726,13 @@ inline constexpr bool method_not_found = false; // class - illegal for a nested class. The overrider is therefore an ordinary // member of the enclosing class, not of KEY. #define BOOST_OPENMETHOD_DETAIL_OVERRIDE_MEM( \ + KEY, REGISTRAR, ID, PARAMETERS, ...) \ + BOOST_OPENMETHOD_DETAIL_OVERRIDE_MEM_AUX( \ + KEY, REGISTRAR, ID, PARAMETERS, \ + ::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); \ @@ -739,7 +779,8 @@ inline constexpr bool method_not_found = false; //! //! @param ID The method's name. //! @param PARAMETERS The overrider's parameter list, in parentheses. -//! @param ... The overrider's return type. +//! @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, ...) \ @@ -755,7 +796,8 @@ inline constexpr bool method_not_found = false; //! //! @param ID The method's name. //! @param PARAMETERS The overrider's parameter list, in parentheses. -//! @param ... The overrider's return type. +//! @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, ...) \ @@ -771,12 +813,13 @@ inline constexpr bool method_not_found = false; //! in. //! @param ID The method's name. //! @param PARAMETERS The overrider's parameter list, in parentheses. -//! @param ... The overrider's return type. +//! @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 \ - ->boost::mp11::mp_back> + auto CLASS::boost_openmethod_overrider_body PARAMETERS->::boost:: \ + openmethod::detail::va_args_no_registry<__VA_ARGS__>::return_type //! Find a member overrider. //! @@ -806,12 +849,14 @@ inline constexpr bool method_not_found = false; //! @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. +//! @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<__VA_ARGS__(*) PARAMETERS>(nullptr))) + static_cast<::boost::openmethod::detail::va_args_no_registry< \ + __VA_ARGS__>::return_type(*) PARAMETERS>(nullptr))) //! Register classes. //! 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/test_member_method.cpp b/test/test_member_method.cpp index 666b0662..1ab3a2d4 100644 --- a/test/test_member_method.cpp +++ b/test/test_member_method.cpp @@ -4,6 +4,7 @@ // or copy at http://www.boost.org/LICENSE_1_0.txt) #include +#include #include #include @@ -166,4 +167,70 @@ BOOST_AUTO_TEST_CASE(member_overrider_private_access_and_next) { 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 + +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(); From fafba2d1631ffd6f93efe15979ee585f0da93ac6 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sun, 20 Sep 2026 16:13:43 -0400 Subject: [PATCH 07/13] macros: do not force-inline the _MEM trampoline on MSVC Every MSVC release job has failed since this branch's first push (run 35455224030, 6207dd3), and it is not a flake: test_member_method.cpp(74): error C2220: the following warning is treated as an error test_member_method.cpp(74): warning C4714: function 'ZooKeeper::openmethod_gensym_19::trampoline>' marked as __forceinline not inlined The trampoline is reached only through `fn`, the function pointer formed from its address on the next line. An explicit `fn(args)` call - which test_member_method.cpp makes, and which is a documented use of BOOST_OPENMETHOD_OVERRIDER_MEM - is therefore an indirect call that cannot be inlined, and MSVC emits C4714 for a __forceinline function it did not inline. The suite builds with /W4 /WX, so it is fatal. It needs /O2 as well as the explicit call, which is why the plain /W4 /WX check run during development missed it: C4714 is only emitted when optimizing. BOOST_FORCEINLINE buys nothing on MSVC anyway - __forceinline is ignored under /Od, so the unoptimized extra call it removes on gcc and clang was never removed there. Guard it, keeping the attribute everywhere it pays. Guarded on BOOST_MSVC rather than _MSC_VER so clang-cl, which does not implement C4714, keeps the attribute. Verified by reproducing the failure locally under the CI condition (cl /std:c++17 /O2 /Ob2 /W4 /WX with an explicit fn call), confirming the guard clears it and the binary still runs, that gcc still expands to inline __attribute__((__always_inline__)), and 204/204 on Linux. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP --- include/boost/openmethod/macros.hpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/include/boost/openmethod/macros.hpp b/include/boost/openmethod/macros.hpp index cd96a5ca..358e75d4 100644 --- a/include/boost/openmethod/macros.hpp +++ b/include/boost/openmethod/macros.hpp @@ -725,6 +725,20 @@ inline constexpr bool method_not_found = false; // 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( \ @@ -739,8 +753,8 @@ inline constexpr bool method_not_found = false; using method_type = \ boost_openmethod_detail_locate_method_aux::type; \ template \ - static BOOST_FORCEINLINE auto trampoline(ForwarderParameters... args) \ - -> __VA_ARGS__ { \ + static BOOST_OPENMETHOD_DETAIL_TRAMPOLINE_INLINE auto trampoline( \ + ForwarderParameters... args) -> __VA_ARGS__ { \ return boost_openmethod_overrider_body( \ static_cast(args)...); \ } \ From 11579acf3abcc05574cb24178b855ae8b6254b3a Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Mon, 21 Sep 2026 10:07:39 -0400 Subject: [PATCH 08/13] macros: keep the _MEM macros usable with a dependent return type 459eb0e routed the _MEM return type through detail::va_args_no_registry<...>::return_type, a nested typedef, which needs `typename` when the return type is dependent. The macro cannot ask the caller for it, so a shape that compiled at 2192199 stopped compiling: template struct Probe { using key = BOOST_OPENMETHOD_OVERRIDER_MEM(S, m, (virtual_ptr), R); }; failed with "expected type-specifier before '::' token" pointing into macros.hpp rather than at the user's line - precisely the diagnostic failure 459eb0e set out to remove. Supply `typename` in all four macros; it has been legal outside a template since C++11. No test covered a dependent return type, which is why the suite stayed green. One is added alongside. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP --- include/boost/openmethod/macros.hpp | 11 ++++++----- test/test_member_method.cpp | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/include/boost/openmethod/macros.hpp b/include/boost/openmethod/macros.hpp index 358e75d4..826f8982 100644 --- a/include/boost/openmethod/macros.hpp +++ b/include/boost/openmethod/macros.hpp @@ -701,7 +701,7 @@ inline constexpr bool method_not_found = false; //! @see [Members and Friends](xref:ROOT:privacy.adoc) #define BOOST_OPENMETHOD_TYPE_MEM(ID, PARAMETERS, ...) \ decltype(BOOST_OPENMETHOD_ID(ID)( \ - static_cast<::boost::openmethod::detail::va_args_no_registry< \ + static_cast::return_type(*) PARAMETERS>(nullptr))) // The overrider's body cannot be named after ID: ID may be qualified (e.g. @@ -743,7 +743,7 @@ inline constexpr bool method_not_found = false; KEY, REGISTRAR, ID, PARAMETERS, ...) \ BOOST_OPENMETHOD_DETAIL_OVERRIDE_MEM_AUX( \ KEY, REGISTRAR, ID, PARAMETERS, \ - ::boost::openmethod::detail::va_args_no_registry< \ + typename ::boost::openmethod::detail::va_args_no_registry< \ __VA_ARGS__>::return_type) #define BOOST_OPENMETHOD_DETAIL_OVERRIDE_MEM_AUX( \ @@ -832,8 +832,9 @@ inline constexpr bool method_not_found = false; //! //! @see [Members and Friends](xref:ROOT:privacy.adoc) #define BOOST_OPENMETHOD_DEFINE_OVERRIDER_MEM(CLASS, ID, PARAMETERS, ...) \ - auto CLASS::boost_openmethod_overrider_body PARAMETERS->::boost:: \ - openmethod::detail::va_args_no_registry<__VA_ARGS__>::return_type + auto CLASS::boost_openmethod_overrider_body PARAMETERS-> \ + typename ::boost::openmethod::detail::va_args_no_registry< \ + __VA_ARGS__>::return_type //! Find a member overrider. //! @@ -869,7 +870,7 @@ inline constexpr bool method_not_found = false; //! @see [Members and Friends](xref:ROOT:privacy.adoc) #define BOOST_OPENMETHOD_OVERRIDER_MEM(CLASS, ID, PARAMETERS, ...) \ decltype(CLASS::boost_openmethod_overrider_key( \ - static_cast<::boost::openmethod::detail::va_args_no_registry< \ + static_cast::return_type(*) PARAMETERS>(nullptr))) //! Register classes. diff --git a/test/test_member_method.cpp b/test/test_member_method.cpp index 1ab3a2d4..64926ce6 100644 --- a/test/test_member_method.cpp +++ b/test/test_member_method.cpp @@ -217,6 +217,26 @@ 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(); From 55621dabba0608d5d52a810165ed14bb9d2d2d4a Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Mon, 21 Sep 2026 10:11:32 -0400 Subject: [PATCH 09/13] macros: name the _MEM members from __LINE__, and hoist the registrar out BOOST_OPENMETHOD_MEM did not work across translation units, which is fatal for it: a member method has nowhere to live but a header. The tag, the alias, the overrider KEY and the registrar were all named with BOOST_OPENMETHOD_GENSYM, i.e. __COUNTER__. Those are class *members*, so a class in a header acquired a different member-specification - and Zoo::poke a different method<> type - in every TU whose counter state differed at the point of inclusion. Two TUs, one with a single `enum { x = __COUNTER__ };` before the include, compiled and linked clean and then reported "not implemented" at the call. It is also a plain ODR violation no compiler diagnoses. Name them from __LINE__ instead: stable for a given header across TUs, and two _MEM macros on one line collide loudly ("member declared twice"), never silently. The free macros were never affected - their tag is ID##_boost_openmethod, and their one gensym names a namespace-scope alias or an internal-linkage variable, neither part of a class's member-specification. The registrar leaves the class body entirely, on the model of inplace_vptr_base's inplace_vptr_use_classes: the trampoline odr-uses detail::mem_registrar, an `inline` variable template keyed on its own type. It therefore needs no invented name at all, and is one entity program-wide - the same overrider seen from any number of TUs registers once, by linkage. test/cross_tu is the first test here built from more than one translation unit, which is why this went uncovered: nothing built from a single .cpp can see it. Verified that it fails against the previous commit's header and passes against this one. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP --- include/boost/openmethod/macros.hpp | 35 +++++++++++++++++++++----- test/CMakeLists.txt | 1 + test/Jamfile | 1 + test/cross_tu/CMakeLists.txt | 27 ++++++++++++++++++++ test/cross_tu/Jamfile | 24 ++++++++++++++++++ test/cross_tu/main.cpp | 28 +++++++++++++++++++++ test/cross_tu/member_method.hpp | 39 +++++++++++++++++++++++++++++ test/cross_tu/other_tu.cpp | 19 ++++++++++++++ 8 files changed, 168 insertions(+), 6 deletions(-) create mode 100644 test/cross_tu/CMakeLists.txt create mode 100644 test/cross_tu/Jamfile create mode 100644 test/cross_tu/main.cpp create mode 100644 test/cross_tu/member_method.hpp create mode 100644 test/cross_tu/other_tu.cpp diff --git a/include/boost/openmethod/macros.hpp b/include/boost/openmethod/macros.hpp index 826f8982..724075e3 100644 --- a/include/boost/openmethod/macros.hpp +++ b/include/boost/openmethod/macros.hpp @@ -96,6 +96,15 @@ struct va_args_no_registry : "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; @@ -103,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 @@ -678,8 +699,8 @@ inline constexpr bool method_not_found = false; //! @see [Members and Friends](xref:ROOT:privacy.adoc) #define BOOST_OPENMETHOD_MEM(ID, PARAMETERS, ...) \ BOOST_OPENMETHOD_DETAIL_MEM( \ - BOOST_OPENMETHOD_GENSYM, BOOST_OPENMETHOD_GENSYM, ID, PARAMETERS, \ - __VA_ARGS__) + 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. @@ -755,6 +776,8 @@ inline constexpr bool method_not_found = false; 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)...); \ } \ @@ -763,8 +786,6 @@ inline constexpr bool method_not_found = false; }; \ static auto boost_openmethod_overrider_key(__VA_ARGS__(*) PARAMETERS) \ ->KEY; \ - static inline KEY::method_type::REGISTRAR \ - BOOST_OPENMETHOD_GENSYM; \ static auto boost_openmethod_overrider_body PARAMETERS->__VA_ARGS__ //! Add an overrider, as a static member function, to a method. @@ -799,7 +820,8 @@ inline constexpr bool method_not_found = false; //! @see [Members and Friends](xref:ROOT:privacy.adoc) #define BOOST_OPENMETHOD_OVERRIDE_MEM(ID, PARAMETERS, ...) \ BOOST_OPENMETHOD_DETAIL_OVERRIDE_MEM( \ - BOOST_OPENMETHOD_GENSYM, inline_override, ID, PARAMETERS, __VA_ARGS__) + BOOST_OPENMETHOD_DETAIL_LINESYM(key_), inline_override, ID, \ + PARAMETERS, __VA_ARGS__) //! Declare a member overrider, without defining it. //! @@ -816,7 +838,8 @@ inline constexpr bool method_not_found = false; //! @see [Members and Friends](xref:ROOT:privacy.adoc) #define BOOST_OPENMETHOD_DECLARE_OVERRIDER_MEM(ID, PARAMETERS, ...) \ BOOST_OPENMETHOD_DETAIL_OVERRIDE_MEM( \ - BOOST_OPENMETHOD_GENSYM, override, ID, PARAMETERS, __VA_ARGS__) + 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. 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/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"; + } +}; From 622b3187da2597a2b910c6c72fbcb187a2fb9c6c Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Mon, 21 Sep 2026 10:31:58 -0400 Subject: [PATCH 10/13] initialize: tell two overriders apart by identity, not by signature augment_methods() merges the copies of an overrider that several state-sharing modules each registered. It keyed that merge on the overrider's *shape* - its function type plus its virtual parameter type ids - gated on the inline_ flag. A shape is not an identity, and two genuinely different overriders share one routinely: namespace a { BOOST_OPENMETHOD_INLINE_OVERRIDE(poke, (virtual_ptr), const char*) { return "a"; } } namespace b { BOOST_OPENMETHOD_INLINE_OVERRIDE(poke, (virtual_ptr), const char*) { return "b"; } } printed "a" and reported no error, where the non-inline spelling of the same pair correctly reports them ambiguous. This is a pre-existing bug, reachable with the free macros alone and with no member method in sight. BOOST_OPENMETHOD_OVERRIDE_MEM only makes it easy to meet: an in-class body is implicitly inline, so every member overrider is flagged inline_, and two classes may each add an overrider of one method with one signature - which the free macros cannot express in a single translation unit. overrider_info gains `identity`, the type id of the registrar itself. Its mangled name carries the registered function as a non-type template argument, so it is distinct between two different overriders and equal between the copies of one. Verified equal across a shared-library boundary under -fvisibility=hidden, which is the case the merge exists to serve. Identity refines the test rather than replacing it, deliberately. It is a type id of a non-class type, and an rtti policy may return one sentinel for every type it does not recognise - the custom_rtti examples returned 0. Keying on identity alone made every overrider in those examples compare equal and collapse into one. As a conjunct it is safe: under such a policy the behaviour is exactly what it was, and under a well-behaved one the false merge is gone. A copy dropped by the merge now has its `next` filled from the copy that was kept. Dispatch reads the variable belonging to the module it runs in, so a dropped copy otherwise left next null while has_next() - which tests only for the not_implemented and ambiguous thunks - reported true. The custom_rtti examples now allocate a distinct id for each non-polymorphic type, counting down from the largest a type_id holds, clear of the ids the Node hierarchy assigns counting up. The RttiFn blueprint said these ids were "for diagnostic and trace purposes", which invited exactly the sentinel that breaks consolidation; it now states that static_type must be injective, and that dynamic_type is under no such obligation. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP --- .../examples/custom_rtti/1/custom_rtti.cpp | 15 +++- .../examples/custom_rtti/2/custom_rtti.cpp | 15 +++- doc/modules/ROOT/pages/custom_rtti.adoc | 24 +++++- include/boost/openmethod/core.hpp | 32 ++++--- include/boost/openmethod/initialize.hpp | 85 ++++++++++++++----- include/boost/openmethod/preamble.hpp | 30 ++++++- ...ntime_errors_inline_overrider_identity.cpp | 59 +++++++++++++ ...ntime_errors_member_overrider_identity.cpp | 57 +++++++++++++ 8 files changed, 272 insertions(+), 45 deletions(-) create mode 100644 test/test_runtime_errors_inline_overrider_identity.cpp create mode 100644 test/test_runtime_errors_member_overrider_identity.cpp 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/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/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/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/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(); From e24a52fc6669393e9e5002642946fe7bdbc1dca6 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Mon, 21 Sep 2026 10:32:17 -0400 Subject: [PATCH 11/13] doc: redirect friends.html to privacy.html, and fix a stale reference f9c0ba3 renamed friends.adoc to privacy.adoc and updated every link in the tree, but nothing re-established the published URL. Both deployed sites currently serve .../openmethod/friends.html, and the rendered output now emits privacy.html only, so after this merges the old path 404s for external bookmarks, mailing-list links and search results - friends.html#friendship among them, and that anchor was deliberately kept inside the new page, so inbound deep links were meant to survive. Antora's :page-aliases: emits the redirect. test_member_method.cpp still named friends.adoc in a comment. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP --- doc/modules/ROOT/pages/privacy.adoc | 1 + test/test_member_method.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/modules/ROOT/pages/privacy.adoc b/doc/modules/ROOT/pages/privacy.adoc index 8b96ec16..1d07bb41 100644 --- a/doc/modules/ROOT/pages/privacy.adoc +++ b/doc/modules/ROOT/pages/privacy.adoc @@ -1,3 +1,4 @@ +:page-aliases: friends.adoc :example: ../examples/rolex [#members] diff --git a/test/test_member_method.cpp b/test/test_member_method.cpp index 64926ce6..d9ba8e54 100644 --- a/test/test_member_method.cpp +++ b/test/test_member_method.cpp @@ -91,7 +91,7 @@ BOOST_AUTO_TEST_CASE(member_method_call_and_overload) { // ---------------------------------------------------------------------------- // Member overriders targeting a FREE method, with private access and no -// friend - the motivating case, mirroring the friends.adoc Payroll example. +// 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). From effbe64ec8c8e47f055fb3b4093404eaad78c6ae Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Mon, 21 Sep 2026 10:35:55 -0400 Subject: [PATCH 12/13] test: a member overrider shared by two modules, under hidden visibility The overrider consolidation this branch reworked had no test that crossed a module boundary, and nothing else in test/ exercises a _MEM macro from more than one module. A member method and a member overrider are declared in a header that both the executable and the shared library include, so each module registers its own copy of the overrider. They are copies of one overrider, not two, and initialize() has to recognise that: keeping both leaves the Dog cell ambiguous, since neither copy is more specific than the other. Built with -fvisibility=hidden, which is what the Boost super-project uses and what makes the test worth having. At default visibility the registrar - an inline variable template since d6bdd83 - is merged across the modules by the dynamic linker, one registration survives, and the consolidation path is never reached. Confirmed by switching BOOST_OPENMETHOD_OVERRIDE_MEM to a non-inline registrar: the test reports ambiguous == 1 and fails, and passes again when it is restored. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP --- test/implicit_shared_libraries/CMakeLists.txt | 1 + test/implicit_shared_libraries/Jamfile | 1 + .../member_overrider/CMakeLists.txt | 50 ++++++++++++++++++ .../member_overrider/Jamfile | 41 +++++++++++++++ .../member_overrider/lib.cpp | 26 ++++++++++ .../member_overrider/lib.hpp | 52 +++++++++++++++++++ .../member_overrider/main.cpp | 37 +++++++++++++ 7 files changed, 208 insertions(+) create mode 100644 test/implicit_shared_libraries/member_overrider/CMakeLists.txt create mode 100644 test/implicit_shared_libraries/member_overrider/Jamfile create mode 100644 test/implicit_shared_libraries/member_overrider/lib.cpp create mode 100644 test/implicit_shared_libraries/member_overrider/lib.hpp create mode 100644 test/implicit_shared_libraries/member_overrider/main.cpp 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(); From 67dd11270acef23d13a524bc35cfa93487835f29 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Mon, 21 Sep 2026 10:50:42 -0400 Subject: [PATCH 13/13] test: pin the signature-collision marker instead of matching anything The marker was `// expected-error: .*`, on the grounds that gcc and clang word the diagnostic differently. That makes the test incapable of failing: openmethod_compile_fail_test sets PASS_REGULAR_EXPRESSION and nothing else, and CTest ignores a test's exit status once that property is set, so `.*` matches whatever comes out - including the output of a file that compiled cleanly. CLAUDE.md sanctions `.*` in place of a `;` inside a regex, not as the whole regex. The wordings do differ - gcc "cannot be overloaded with", clang "class member cannot be redeclared", MSVC C2556 "overloaded function differs only by return type" - but all three name boost_openmethod_overrider_key, the accessor whose overload set the collision happens in. Verified on gcc 13, clang 18 and MSVC v18. Checked that the test now fails when it should: giving the second overrider a different parameter list makes the file compile, and the test reports "Required regular expression not found". Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP --- ...le_fail_member_overrider_signature_collision.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/test/compile_fail_member_overrider_signature_collision.cpp b/test/compile_fail_member_overrider_signature_collision.cpp index 012ed921..1ccc43b4 100644 --- a/test/compile_fail_member_overrider_signature_collision.cpp +++ b/test/compile_fail_member_overrider_signature_collision.cpp @@ -3,10 +3,15 @@ // See accompanying file LICENSE_1_0.txt // or copy at http://www.boost.org/LICENSE_1_0.txt) -// The two compilers disagree on the wording for this one: gcc says "cannot -// be overloaded with", clang says "class member cannot be redeclared" - no -// common substring, hence the `.*`. -// expected-error: .* +// 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