Skip to content

Add reference qualifier support - #356

Draft
RyanJK5 wants to merge 12 commits into
mainfrom
value-categories
Draft

RyanJK5 wants to merge 12 commits into
mainfrom
value-categories

Conversation

@RyanJK5

@RyanJK5 RyanJK5 commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

Closes #97 and #268

@RyanJK5
RyanJK5 requested a review from jbcoe as a code owner September 12, 2026 05:33
@RyanJK5
RyanJK5 marked this pull request as draft September 12, 2026 05:33
@RyanJK5

RyanJK5 commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator Author

I made the following design decisions while implementing this PR. Happy to discuss/change any of them as appropriate. The tests cover these edge cases should we want to modify behavior.

  • An unqualified interface function can match an lvalue-qualified member function, because the dispatch assumes the underlying type is an lvalue anyway unless otherwise specified. Therefore, an interface with int foo() will match a candidate function int foo() &. A candidate function int foo() && will not conform, however.
  • Lvalue- and rvalue-qualified overloads of the same function can exist in an interface. They will provide two different dispatch paths depending on the value category of protocol. Reference qualification can also be applied to const interface member functions (though there's probably little reason to do this).
  • protocol_view<I> and protocol_view<const I> do NOT propagate value category. They will both only pull in unqualified interface members and lvalue-qualified interface members. Rvalue interface members are not included. Calling a member function from an rvalue protocol_view will always dispatch to the lvalue overload. This follows the precedent of const propagation.
  • An unqualified or lvalue-qualified interface function will match functions with an lvalue-qualified explicit object parameter. It will NOT match functions with an rvalue-qualified explicit object parameter; an rvalue-qualified interface function will, however.
  • static candidate functions will match regardless of the reference qualification of the interface function.

I tried to keep the architecture mostly intact, but refactored some things to avoid passing a million distinct booleans around everywhere. Any feedback is much appreciated!

@codecov

codecov Bot commented Sep 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.90909% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.46%. Comparing base (6eeb9ca) to head (1088583).

Files with missing lines Patch % Lines
protocol.hh 90.90% 1 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #356      +/-   ##
==========================================
+ Coverage   74.06%   74.46%   +0.39%     
==========================================
  Files           9        9              
  Lines         860      881      +21     
  Branches      214      216       +2     
==========================================
+ Hits          637      656      +19     
  Misses         24       24              
- Partials      199      201       +2     
Flag Coverage Δ
consteval 97.70% <94.73%> (+0.11%) ⬆️
runtime 71.91% <88.00%> (+0.35%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@philipcraig philipcraig left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran the tests and a few probes on GCC trunk. Inline comments cover the conformance rules, the trampolines and the thunk layout. Three things sit outside the diff:

  • Stale comments: protocol_interface_function_infos (line 236) still says ref-qualified members are rejected and points at a TODO on method_thunk that this PR deletes; is_protocol_conformant (line 802) still explains why it calls the function instead of reading the variable in terms of a throw that no longer happens; make_view_vtable (line 739) still says all_const populates every entry.
  • conformance_check_rejects in protocol_test.cc (line 407) has no callers left and its comment describes the old rejection. Deleting the #ifdef __cpp_constexpr_exceptions block with it. A positive test for the deleted static case would be worth keeping: struct I { int take() &&; }; struct C { static int take(); }; conforms and dispatches through protocol<I>.
  • Docs: the Limitations bullet in CONTRIBUTING.md (line 190) still says the generated wrapper does not apply the qualifier, and DRAFT.md says nothing about value-category propagation or about views omitting && members, which #97 asked the draft to acknowledge.

Comment thread protocol.hh Outdated
Comment thread protocol.hh
// parameter types; noexcept is not compared.
consteval bool same_signature(std::meta::info candidate,
std::meta::info interface) {
if (is_rvalue_reference_qualified(interface) !=

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only the && qualifier is compared, so & is never checked in either direction and #268 is answered implicitly. With struct A { int foo() & { return 2; } int foo() && { return 1; } }; against interface int foo(), std::move(p).foo() returns 2 while std::move(a).foo() returns 1. A &-only candidate also conforms to int foo() even though moving the concrete object is ill-formed, so the protocol is more permissive than the type. The comment above says "cvref qualifiers" and the test is named RefQualifiersMatchExactly, but & is not part of the rule.

Either pick one of the #268 options here (reject &-only candidates for unqualified members, or two vtable entries) and say so in this comment, the member_policy::propagate comment and DRAFT.md, or reword the comment and test name to state the rule as implemented.

Comment thread protocol.hh
if (is_const(interface) != is_const(remove_reference(obj_type))) return false;

if (is_rvalue_reference_qualified(interface) !=
is_rvalue_reference_type(obj_type))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other direction is stricter than it needs to be. For interface int take() &&, an unqualified candidate int take() is rejected here and at line 148, and a by-value explicit-object int take(this C self) is rejected too, while the static branch accepts static int take(). Any ordinary type, including std types, therefore cannot satisfy an && interface even though the trampoline's std::move(*ptr).take() would dispatch correctly. A callability-based rule (the candidate conforms if it is callable in every value category the interface member admits) fixes this and the & case above together.

Comment thread protocol_test.cc Outdated
Comment thread protocol.hh
find_conforming_member<member, ^^U>();
result.[:vtable_member:] = &mutable_view_trampoline<FnPtrType, U,

// View vtables cannot use rvalue member functions.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The drop is silent. is_valid_view_interface still accepts an interface whose only members are &&-qualified, so struct I { int f() &&; }; protocol_view<I> v(c); compiles and v has no member f at all; the first diagnostic is "no member named f" at the call site. Either reject names that would vanish under all_const/const_only in is_valid_view_interface or generate_wrapper_bases, or state the rule on protocol_view and in DRAFT.md.

Comment thread protocol.hh
struct wrapper_bases : MemberBases... {};

// Returns the appropriate const, noexcept, and reference qualification for the
// member based on the const policy.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"const policy" is the old name; this now takes a member_policy.

Comment thread protocol.hh
// viewed/owned object.
R operator()(Args... args) noexcept(IsNoexcept)
requires(!IsConst)
R operator()(Args... args) & noexcept(is_noexcept)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The four operator() bodies differ only in the const on the enclosing() cast, so the valueless assert, the vtable load and the splice call now have to change in four places. A private static

template <typename P>
static R call(P* protocol_object, Args&&... args) noexcept(is_noexcept);

holding the body, with each qualified overload a one-line return call(static_cast<ProtocolType*>(enclosing(this)), std::forward<Args>(args)...);, passes the PR's test file and removes about thirty lines. A single deducing-this operator() is not a drop-in: under all_const, p.foo() becomes ambiguous between the & and const& thunks brought in by member_thunk's using-declarations.

Comment thread protocol.hh
std::string(name) + "'");
}

enum class member_options {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two of these flags are derivable from Member (is_noexcept, and is_const under propagate), and encoding "unqualified" as is_lvalue | is_rvalue means every unqualified member and every view member now gets two constrained ref-qualified overloads where one unqualified or const operator() did before. On GCC trunk the unchanged pre-PR test corpus compiles about 10% slower with the new header, and a 40-member TU calling members in both value categories emits 180 thunk bodies instead of 100 at -O0 (identical at -O2). The static members is_noexcept/is_const also shadow std::meta::is_noexcept/is_const inside method_thunk.

Passing overload_spec<Member, member_policy> and deriving the booleans inside method_thunk, with a three-state ref qualifier (none, &, &&), keeps the single operator() for unqualified and view members. GCC trunk accepts mutually exclusive constrained unqualified and ref-qualified overloads in one class, so the thunk does not need splitting.

@philipcraig

Copy link
Copy Markdown
Collaborator

Thanks for listing the decisions, that made them easy to check. Three notes beyond the inline review.

Decision 1 and #268. On #268 @jbcoe preferred A to be non-conforming for now, since relaxing later is free and tightening later breaks users. This PR goes the other way: int foo() & conforms to int foo(), so std::move(p).foo() compiles on the protocol while std::move(a).foo() does not on the concrete type. Worth settling explicitly before this closes #268.

One rule for all five. The decisions fall out of a single statement: a candidate conforms if it is callable in every value category the interface member admits. That gives: unqualified interface members accept unqualified, const, static and by-value explicit-object candidates, and reject &-only and &&-only ones; && interface members additionally accept unqualified candidates (which the current code rejects at line 148/173 while accepting static ones); & interface members accept &, unqualified, static and this C&. It also answers #97's request for a sentence in DRAFT.md.

Legal overload sets. [over.load]/2.3 forbids mixing ref-qualified and unqualified overloads of one name, so once an interface has foo() & it must spell the const overload foo() const&, not foo() const. GCC trunk and clang-p2996 do not diagnose this (GCC 15 does), which is why OverloadedQualifiers compiles. That is a user-facing constraint on interface shape and belongs next to the value-category text in DRAFT.md.

On views I agree with not propagating, though for a different reason than the const precedent: a view is a handle, and moving the handle says nothing about the viewed object, as with a moved std::reference_wrapper. The follow-on is that a name which vanishes entirely from protocol_view<I> should fail at instantiation rather than silently.

@jbcoe

jbcoe commented Sep 12, 2026

Copy link
Copy Markdown
Owner

If we did want r-value-qualified member functions called from views, we probably want protocol_view<T&&> but I'm not sure why anyone would want that.

@jbcoe jbcoe left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good but needs rebasing onto #355 which I'm happy to do when it lands.

@jbcoe jbcoe self-assigned this Sep 12, 2026
RyanJK5 and others added 2 commits September 13, 2026 12:00
Co-authored-by: Philip Craig <689193+philipcraig@users.noreply.github.com>
Co-authored-by: Philip Craig <689193+philipcraig@users.noreply.github.com>
@RyanJK5

RyanJK5 commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator Author

@jbcoe I think this has probably fallen far enough behind that it's worth doing over, but curious what you think.

@jbcoe

jbcoe commented Sep 18, 2026

Copy link
Copy Markdown
Owner

If you'd like to pair-program a painful manual rebase over coffee it could be instructive, or character building.

@RyanJK5

RyanJK5 commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator Author

If you'd like to pair-program a painful manual rebase over coffee it could be instructive, or character building.

That would certainly wake me up in the morning. Any preferred times?

@jbcoe

jbcoe commented Sep 20, 2026

Copy link
Copy Markdown
Owner

@RyanJK5 I'm sorry that we never got to rebase this face-to-face. Would you like me to rebase this PR on the plane?

@RyanJK5

RyanJK5 commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

@RyanJK5 I'm sorry that we never got to rebase this face-to-face. Would you like me to rebase this PR on the plane?

No worries. If you're up for it, that would be really helpful.

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.

Handle value category propagation

3 participants