Add assignment to sum, with the strong exception guarantee#304
Merged
Conversation
This was referenced Jul 13, 2026
🤖 Augment PR SummarySummary: This PR makes Changes:
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 👎 |
ebc910e to
661392a
Compare
59cc227 to
25981c1
Compare
661392a to
19eaeaf
Compare
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]
25981c1 to
b729a01
Compare
Member
Author
|
augment review |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
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]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Without it
fn::expected<T, fn::sum<E1, E2>>- the graded monad the sum exists to serve - was not assignable at all._reinitmirrors 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
Tsbeyond what construction already asks, and a type with no assignment operator at all is still assignable through the sum.choiceadds 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 reasonsumcannot 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 tomainonce 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 ofmain, and the aggregate branch ran the full CI matrix green (#289).