Skip to content

Let interfaces control movability - #280

Draft
RyanJK5 wants to merge 4 commits into
mainfrom
restrict-movability
Draft

Let interfaces control movability#280
RyanJK5 wants to merge 4 commits into
mainfrom
restrict-movability

Conversation

@RyanJK5

@RyanJK5 RyanJK5 commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Left in draft because this poses some important design questions.

Closes #187 and #193.

@RyanJK5
RyanJK5 requested a review from jbcoe as a code owner September 1, 2026 18:19
@RyanJK5
RyanJK5 marked this pull request as draft September 1, 2026 18:19
@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 72.90%. Comparing base (890f625) to head (5ff9958).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #280      +/-   ##
==========================================
+ Coverage   72.87%   72.90%   +0.03%     
==========================================
  Files           9        9              
  Lines         822      823       +1     
  Branches      208      208              
==========================================
+ Hits          599      600       +1     
  Misses         24       24              
  Partials      199      199              
Flag Coverage Δ
consteval 97.46% <ø> (ø)
runtime 70.29% <100.00%> (+0.03%) ⬆️

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.

@RyanJK5

RyanJK5 commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

We could allow move construction unconditionally and just prevent allocator-aware move construction and move assignment. Do we want to do this?

@jbcoe

jbcoe commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Polymorphic (owning) is unconditionally move constructible, I think we need to do the same for protocol for consistency with other standard library types.

@RyanJK5

RyanJK5 commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

Polymorphic (owning) is unconditionally move constructible, I think we need to do the same for protocol for consistency with other standard library types.

Are you leaning more towards gating movability requirements based on the allocator than the interface?

An alternative approach we could take is allow unconditional move construction and assignment, UNLESS the user-provided allocator explicitly needs to know how to generate the move() function (i.e. POCMA or always_equal is false)

@jbcoe

jbcoe commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Any U in polymorphic must be copyable. We don't have this constraint in protocol so have more freedom than I'd initially thought.

We should enumerate and evaluate our options.

@RyanJK5

RyanJK5 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Any U in polymorphic must be copyable. We don't have this constraint in protocol so have more freedom than I'd initially thought.

We should enumerate and evaluate our options.

Happy to discuss further. As I see it, the choices are to allow the interface to control this capability, or to make it dependent on the allocator.

@jbcoe

jbcoe commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Do we want to flesh these options out in the comments and show code that would work and not work under the various options? GitHub markdown is pretty powerful.

@RyanJK5

RyanJK5 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Do we want to flesh these options out in the comments and show code that would work and not work under the various options? GitHub markdown is pretty powerful.

One approach is to let the interface control movability, like this PR adds:

struct Movable {};

protocol<Movable> p(SomeCopyableType{});
auto p2 = std::move(p); // moving allowed
auto p3 = p2; // copying allowed
protocol<Movable> p4(SomeMoveOnlyType{}); // ERROR

struct Immovable {
    Immovable(const Immovable&) = delete;
    Immovable(Immovable&&) = delete;
};

protocol<Immovable> p4(SomeMoveOnlyType{}); // OK
protocol p5(std::move(p4)); // ERROR (moving)
auto p6 = p5; // ERROR (copying)
p4 = SomeCopyableType{}; // OK

We can also recognize, however, that the only time we need to actually call the move constructor of the object is when we dispatch to the move() function in the vtable. This only occurs when we need to reconstruct the object from an unequal allocator, so we can guarantee the object won't be moved in the same situations where we can guarantee move construction / assignment is noexcept.

So this alternative approach would mean:

  • move constructor: available always.
  • allocator-aware move construction: available if the interface is movable, OR if the allocator is_always_equal.
  • move assignment: available if the interface is movable, OR if the allocator is_always_equal OR if the allocator has pocma.

I think we still need the interface to constrain movability in this case, as that is the user opting in to say "I care about allocator-aware move construction and move assignment" and therefore requiring movability from a conforming T in the case where the allocator is non-standard.

An example:

// pretend there's allocator types PocmaFalseAlwaysEq and PocmaTrueNotAlwaysEq

protocol<Movable> p(SomeMoveOnlyType{}); // No ERROR, because std::allocator is_always_equal

protocol<Movable, PocmaFalseAlwaysEq> p1(std::allocator_arg, /*whatever*/, SomeMoveOnlyType{}); // No ERROR, because always equal

protocol<Movable, PocmaTrueNotAlwaysEq> p2(std::allocator_arg, /*whatever*/, SomeMoveOnlyType{}); // ERROR, because allocator-aware move construction would not work.

protocol<Immovable> p3(SomeMoveOnlyType{}); // OK
protocol p4(std::move(p3)); // OK
p3 = std::move(p4); // OK

protocol<Immovable, PocmaTrueNotAlwaysEq> p5(SomeMoveOnlyType{}); // OK
protocol p6(std::move(p5)); // OK
p5 = std::move(p6); // OK because pocma is true.
protocol p7(std::allocator_arg, /* a new allocator */, std::move(p5)); // ERROR because always_equal is false

// If we had PocmaFalseNotAlwaysEq, both move assignment AND alloc-aware move construction would be disabled.

I don't think these are necessarily the only methods, but these are the ones I thought of.

@RyanJK5

RyanJK5 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

This also has some implications for SBO. If the interface requires movability, the SBO can only be used if the type is movable. If the interface does not, SBO can be used for immovable types.

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.

Revisit move construction design

2 participants