Skip to content

overriders: BOOST_OPENMETHOD_MEM family for methods/overriders as static members (#53) - #122

Open
jll63 wants to merge 13 commits into
boostorg:developfrom
jll63:mem-alt
Open

jll63 wants to merge 13 commits into
boostorg:developfrom
jll63:mem-alt

Conversation

@jll63

@jll63 jll63 commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

Alternative design for #53, exploring a different API shape than #120
(feature/member-overriders). Independent branches - neither depends on the
other, and this is deliberately a separate PR to compare approaches, not a
replacement.

Six new 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, exactly as for a free method.

  • BOOST_OPENMETHOD_MEM declares a method as a static member function,
    overloadable exactly like a free method (its tag is generated per
    expansion, not 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.
  • 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 (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.

No core.hpp changes: each overrider registers 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.

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. Pinned by
test/compile_fail_member_overrider_signature_collision.cpp.

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.

Test plan

  • ctest - 203/203 passing (197 baseline + test_member_method.cpp (2
    cases) + 3 new compile-fail tests)
  • Verified on gcc 13, clang 18, and MSVC 19.51/BuildTools v18 (the
    design leans on nested-class complete-class-context bodies and
    function-template address deduction, both flagged as MSVC risk during
    design)
  • doc/build_antora.sh - full site build, no new warnings; verified no
    stray backticks and no MrDocs artifact leakage in the touched/new
    pages and six new reference pages
  • rolex/8 example and member.cpp snippet run manually, output
    matches expected ($5000 / $10000 / $985000, bark/hiss)

🤖 Generated with Claude Code

@cppalliance-bot

cppalliance-bot commented Sep 19, 2026

Copy link
Copy Markdown

An automated preview of the documentation is available at https://122.openmethod.prtest3.cppalliance.org/libs/openmethod/doc/html/index.html

If more commits are pushed to the pull request, the docs will rebuild at the same URL.

2026-09-21 18:52:13 UTC

jll63 and others added 13 commits September 21, 2026 14:40
…tic members (boostorg#53)

Alternative design to boostorg#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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP
…rence

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP
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<Payroll&>` 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP
…acros

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP
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<virtual_ptr<Cat, ...>>'
    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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP
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<class R> struct Probe {
      using key = BOOST_OPENMETHOD_OVERRIDER_MEM(S, m, (virtual_ptr<D>), 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP
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<R>, 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP
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<Dog>),
                                                   const char*) { return "a"; } }
    namespace b { BOOST_OPENMETHOD_INLINE_OVERRIDE(poke, (virtual_ptr<Dog>),
                                                   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<Fn> null while
has_next<Fn>() - 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E53cDWKgiva4cfH48EtvMP

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants