Skip to content

Add assignment to sum, with the strong exception guarantee#304

Merged
Bronek merged 1 commit into
mainfrom
bronek/P10/sum_assignment
Jul 13, 2026
Merged

Add assignment to sum, with the strong exception guarantee#304
Bronek merged 1 commit into
mainfrom
bronek/P10/sum_assignment

Conversation

@Bronek

@Bronek Bronek commented Jul 13, 2026

Copy link
Copy Markdown
Member

Without it fn::expected<T, fn::sum<E1, E2>> - the graded monad the sum exists to serve - was not assignable at all.

_reinit mirrors reinit-expected ([expected.object.assign]): a sum always holds one of its alternatives, and there is no valueless state to fall back on, so the one it holds is destroyed only once its replacement is certain. Which arm applies is decided per ALTERNATIVE, not for the sum as a whole - a nothrow-copyable one is built straight over the old, and one whose copy can throw is copied into a temporary first, where only its (nothrow) move goes near the storage. A sum whose alternatives need different arms is therefore still assignable, and the spec is the conjunction of what each arm promises.

The standard's third arm - snapshot the old alternative, roll back on throw - has nothing to offer a sum. It would exist for an alternative that can neither be built nor moved without throwing; but every alternative is also a possible OLD alternative, since one is assigned over itself whenever the sum already holds it, and snapshotting that same type would throw in turn. There is no safe path, so such an alternative is constrained away rather than half-served.

The alternative being replaced is CONSTRUCTED over, never assigned to - even when it does not change. Assignment therefore asks nothing of Ts beyond what construction already asks, and a type with no assignment operator at all is still assignable through the sum.

choice adds no state of its own, so it only has to declare the two operators to inherit all of this: its move constructor would otherwise have deleted the implicit copy assignment and suppressed the implicit move assignment - the same reason sum cannot rely on implicit ones either.

Closes #183


Stack: P10 of 13 — the release-0.1 bugfix queue, merging bottom-up into main. Based on #303 and to be retargeted to main once it merges; the diff shows only this PR's commits. Every stack boundary was validated with a gcc build and the full test suite on top of main, and the aggregate branch ran the full CI matrix green (#289).

@augmentcode

augmentcode Bot commented Jul 13, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: This PR makes fn::sum (and therefore fn::choice and fn::expected<T, fn::sum<...>>) assignable while targeting the strong exception guarantee.

Changes:

  • Adds <memory> and implements sum::_reinit<T> to rebuild the active alternative via either a direct nothrow construction path or a temporary-then-move path.
  • Adds a constrained sum::operator=(sum const&) that only requires construction (not alternative assignment operators) and computes noexcept from copy construction.
  • Adds a constrained sum::operator=(sum&&) that is only provided when all alternatives are nothrow move-constructible.
  • Declares defaulted copy/move assignment operators on choice so it inherits sum’s constraints/guarantees.
  • Extends tests for sum, choice, and graded expected to cover assignment behavior, noexcept traits, constexpr usage, self-assignment, destruction of replaced alternatives, exception safety, and constraints.

Technical Notes: Assignment reconstructs the alternative (never assigns into it), and rvalue assignment can fall back to copy assignment when move assignment is constrained away.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review completed. 1 suggestion posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread include/fn/sum.hpp
@Bronek Bronek force-pushed the bronek/P9/relocating_verbs_constraints branch 2 times, most recently from ebc910e to 661392a Compare July 13, 2026 16:44
@Bronek Bronek force-pushed the bronek/P10/sum_assignment branch from 59cc227 to 25981c1 Compare July 13, 2026 16:44
@Bronek Bronek force-pushed the bronek/P9/relocating_verbs_constraints branch from 661392a to 19eaeaf Compare July 13, 2026 17:14
@Bronek Bronek changed the base branch from bronek/P9/relocating_verbs_constraints to main July 13, 2026 17:31
Without it `fn::expected<T, fn::sum<E1, E2>>` - the graded monad the sum exists
to serve - was not assignable at all.

`_reinit` mirrors reinit-expected ([expected.object.assign]): a sum always holds
one of its alternatives, and there is no valueless state to fall back on, so the
one it holds is destroyed only once its replacement is certain. Which arm applies
is decided per ALTERNATIVE, not for the sum as a whole - a nothrow-copyable one
is built straight over the old, and one whose copy can throw is copied into a
temporary first, where only its (nothrow) move goes near the storage. A sum whose
alternatives need different arms is therefore still assignable, and the spec is
the conjunction of what each arm promises.

The standard's third arm - snapshot the old alternative, roll back on throw - has
nothing to offer a sum. It would exist for an alternative that can neither be
built nor moved without throwing; but every alternative is also a possible OLD
alternative, since one is assigned over itself whenever the sum already holds it,
and snapshotting that same type would throw in turn. There is no safe path, so
such an alternative is constrained away rather than half-served.

The alternative being replaced is CONSTRUCTED over, never assigned to - even when
it does not change. Assignment therefore asks nothing of `Ts` beyond what
construction already asks, and a type with no assignment operator at all is still
assignable through the sum.

`choice` adds no state of its own, so it only has to declare the two operators to
inherit all of this: its move constructor would otherwise have deleted the
implicit copy assignment and suppressed the implicit move assignment - the same
reason `sum` cannot rely on implicit ones either.

Closes #183

Assisted-by: Claude:claude-opus-4-8[1m]
@Bronek Bronek force-pushed the bronek/P10/sum_assignment branch from 25981c1 to b729a01 Compare July 13, 2026 17:34
@Bronek

Bronek commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

augment review

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review completed. No suggestions at this time.

Comment augment review to trigger a new review at any time.

@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.00000% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
include/fn/sum.hpp 83.3% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

@sonarqubecloud

Copy link
Copy Markdown

@Bronek Bronek merged commit 5aa0de3 into main Jul 13, 2026
63 of 64 checks passed
Bronek added a commit that referenced this pull request Jul 13, 2026
Take #303's relocation constraints and #304's sum assignment - both additive,
so the reconciliation is placement rather than flips: the constraints cases
join the verb files, the assignment cases sum and choice, and expected gains
the graded shape that could not be assigned at all before.

One assertion of ours had to go the other way. #303 asks what value_or
actually relocates, and it is the value alone - the error is discarded, never
carried into the result - so its throwing copy no longer weighs, and the
comment claiming it did described the old imprecision as if it were the
design.

Assisted-by: Claude:claude-opus-4-8[1m]
Bronek added a commit that referenced this pull request Jul 13, 2026
Take #303's relocation constraints and #304's sum assignment - both additive,
so the reconciliation is placement rather than flips: the constraints cases
join the verb files, the assignment cases sum and choice, and expected gains
the graded shape that could not be assigned at all before.

One assertion of ours had to go the other way. #303 asks what value_or
actually relocates, and it is the value alone - the error is discarded, never
carried into the result - so its throwing copy no longer weighs, and the
comment claiming it did described the old imprecision as if it were the
design.

Assisted-by: Claude:claude-opus-4-8[1m]
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.

Add operator= to fn::sum, strong exception guarantee

1 participant