Skip to content

Ask of a sum alternative the same question its initialization answers#305

Merged
Bronek merged 5 commits into
mainfrom
bronek/P11/initialization_questions
Jul 13, 2026
Merged

Ask of a sum alternative the same question its initialization answers#305
Bronek merged 5 commits into
mainfrom
bronek/P11/initialization_questions

Conversation

@Bronek

@Bronek Bronek commented Jul 13, 2026

Copy link
Copy Markdown
Member

The #291 sweep: constraints and specifications derived from the initialization the code performs, at the layer performing it - sum's storage, choice's inheritance, pack's aggregate machinery - each pinned by tests so it cannot drift again.

Ask of a sum alternative the same question its initialization answers

A sum brace-initializes the alternative it stores, but its constraints and noexcept specifications asked std::is_[nothrow_]constructible, which is a question about parentheses. The two disagree - braced initialization considers initializer-list constructors first - and where they disagree the parenthesized answer is a lie: a type can promise a nothrow move and still throw from T{std::move(t)}.

Believing it cost more than a wrong specification. sum's move constructor was declared noexcept on that promise and called std::terminate when the promise broke; and _reinit, whose temporary arm rests on the move being nothrow, destroyed the alternative it held and then failed to replace it - leaving the sum with no alternative at all, which is not a lost guarantee but a dead object.

So every trait that constrains or specifies the construction of an alternative now asks the brace question, through _initializable / _nothrow_initializable - the traits that already exist for it.

The choice of braces is a design direction rather than an implementation detail - it is what gives sum aggregate forwarding through brace elision, which std::variant cannot express and parenthesized initialization cannot either - so a test now pins it, and says what it buys and what it costs. Other tests would break if it were switched; that one breaks on purpose.

The temporary arm's completion, as opposed to its throwing, had no coverage at all: only the throw was ever exercised.

Derive what storing an alternative costs from the code that stores it

The constraints and specifications of sum restated what its storage does, and a restatement can drift from the deed - which is how std::is_nothrow_move_constructible_v came to answer for T{std::move(t)}, a question it does not ask.

make_variadic_union is the only thing that ever builds an alternative, so it now says what it can build and what that costs: constrained on the initialization it performs, and specified by it. Everything above asks that, through the two concepts beside it, rather than describing it again. sum names the operations once - copyable, movable, and their nothrow twins - and every constructor, assignment and _reinit is written in those terms. choice and the as_sum lifts ask the sum they construct, which asks its storage in turn. No declaration restates the initialization, and none can disagree with it.

The arm that assignment takes is still chosen per alternative, so _copy_assignable keeps that choice inside the fold: one arm serving them all would reject a sum whose alternatives simply need different ones.

make_variadic_union also declares its return type now. Deducing it meant instantiating the body to deduce it, and the body odr-uses the alternative's constructor - so merely ASKING whether an alternative could be stored demanded a definition of the constructor that would store it.

Pin what choice's defaulted special members follow

choice adds no state to the sum it derives from, and defaults all five of its special members - so each must behave exactly as the base's, down to its noexcept and its constraints. Nothing said so, and nothing would have noticed if it stopped being true: dropping a = default, promising a noexcept the base does not, or narrowing a requires-clause would all have passed in silence.

They are compared against sum's across the property axes that make the answers differ - a throwing copy, a move-only alternative, an immovable one - and each comparison is backed by what the answer actually is, so that an equality cannot be satisfied by both sides being wrong at once.

Derive what pack's storage performs from the code that performs it

The pack half of #291. Relocation asked is_nothrow_constructible_v - direct-initialization - where the aggregate initialization in _append copy-initializes each element from its source ([dcl.init.aggr]/4.3), which cannot reach an explicit constructor: an element with an explicit nothrow move and a throwing copy made append promise a noexcept the deed cannot keep. Nothing asked whether relocation was possible at all, so appending to a pack whose element cannot make the move hard-errored inside the body where a clean rejection was owed. as_pack asked the same question of pack itself, where parenthesized initialization performs no brace elision - false for every argument list, a permanent under-promise. invoke_r promised direct-initialization of Ret and performed a return statement.

_make_element now declares its own contract, _relocatable_element asks the element holder for the copy-initialization it performs, _append is constrained on and specified by both questions, and every append overload asks the _append it calls. as_pack is constrained on and specified by the braces it performs, and declares its return type. invoke_r delegates to the new pack_impl::_invoke_r, asking the same INVOKE<R> question sum::invoke_r asks - which also aligns invoke_r<void>, accepted by std::invoke_r and sum and until now rejected by pack.

Pin what pack is: an aggregate, at every layer

pack declares no constructor and no special member - _element, pack_impl and pack itself are aggregates, and everything above rests on that: brace initialization with elision, the structural type, and special members composed from the elements' own. A constructor added anywhere in the stack would change all of it in silence; these assertions fail instead.

Closes #291


Stack: P11 of 13 — the release-0.1 bugfix queue, merging bottom-up into main. Based on #304 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 aligns sum/choice/pack constraints and noexcept specifications with the exact initialization forms they actually perform (notably brace-initialization and aggregate relocation), preventing trait/spec drift.

Changes:

  • Reworks sum to derive copy/move/assignment feasibility and noexcept from detail::make_variadic_union via new _makeable/_nothrow_makeable concepts
  • Makes make_variadic_union declare its return type and constrain/declare noexcept based on T{...} (brace) initialization
  • Updates choice constructors’ constraints/noexcept to ask constructibility of the base sum implementation rather than restating alternative traits
  • Refactors pack element construction/relocation and append constraints to match aggregate copy-initialization and brace elision semantics
  • Routes pack::invoke_r through a new pack_impl::_invoke_r to match INVOKE<R>-style convertibility/noexcept behavior

Technical Notes: Adds targeted tests that pin (1) “braces, not parentheses” for sum, (2) defaulted choice special members matching the base, and (3) pack’s aggregate nature and relocation/noexcept edge cases to prevent future regressions.

🤖 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. No suggestions at this time.

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

@Bronek Bronek force-pushed the bronek/P11/initialization_questions branch from 2f50521 to 68e5e78 Compare July 13, 2026 16:44
@Bronek Bronek force-pushed the bronek/P10/sum_assignment branch 2 times, most recently from 25981c1 to b729a01 Compare July 13, 2026 17:34
@Bronek Bronek changed the base branch from bronek/P10/sum_assignment to main July 13, 2026 19:39
Bronek added 5 commits July 13, 2026 20:44
A sum brace-initializes the alternative it stores, but its constraints and
noexcept specifications asked `std::is_[nothrow_]constructible`, which is a
question about parentheses. The two disagree - braced initialization considers
initializer-list constructors first - and where they disagree the parenthesized
answer is a lie: a type can promise a nothrow move and still throw from
`T{std::move(t)}`.

Believing it cost more than a wrong specification. `sum`'s move constructor was
declared `noexcept` on that promise and called std::terminate when the promise
broke; and `_reinit`, whose temporary arm rests on the move being nothrow,
destroyed the alternative it held and then failed to replace it - leaving the sum
with no alternative at all, which is not a lost guarantee but a dead object.

So every trait that constrains or specifies the construction of an alternative
now asks the brace question, through `_initializable` / `_nothrow_initializable`
- the traits that already exist for it.

The choice of braces is a design direction rather than an implementation detail -
it is what gives `sum` aggregate forwarding through brace elision, which
`std::variant` cannot express and parenthesized initialization cannot either - so
a test now pins it, and says what it buys and what it costs. Other tests would
break if it were switched; that one breaks on purpose.

The temporary arm's completion, as opposed to its throwing, had no coverage at
all: only the throw was ever exercised.

Closes #291

Assisted-by: Claude:claude-opus-4-8[1m]
The constraints and specifications of `sum` restated what its storage does, and a
restatement can drift from the deed - which is how `std::is_nothrow_move_constructible_v`
came to answer for `T{std::move(t)}`, a question it does not ask.

`make_variadic_union` is the only thing that ever builds an alternative, so it now
says what it can build and what that costs: constrained on the initialization it
performs, and specified by it. Everything above asks that, through the two
concepts beside it, rather than describing it again. `sum` names the operations
once - copyable, movable, and their nothrow twins - and every constructor,
assignment and `_reinit` is written in those terms. `choice` and the `as_sum`
lifts ask the sum they construct, which asks its storage in turn. No declaration
restates the initialization, and none can disagree with it.

The arm that assignment takes is still chosen per alternative, so `_copy_assignable`
keeps that choice inside the fold: one arm serving them all would reject a sum
whose alternatives simply need different ones.

`make_variadic_union` also declares its return type now. Deducing it meant
instantiating the body to deduce it, and the body odr-uses the alternative's
constructor - so merely ASKING whether an alternative could be stored demanded a
definition of the constructor that would store it.

Closes #291

Assisted-by: Claude:claude-opus-4-8[1m]
`choice` adds no state to the `sum` it derives from, and defaults all five of its
special members - so each must behave exactly as the base's, down to its noexcept
and its constraints. Nothing said so, and nothing would have noticed if it stopped
being true: dropping a `= default`, promising a `noexcept` the base does not, or
narrowing a requires-clause would all have passed in silence.

They are compared against `sum`'s across the property axes that make the answers
differ - a throwing copy, a move-only alternative, an immovable one - and each
comparison is backed by what the answer actually is, so that an equality cannot be
satisfied by both sides being wrong at once.

Closes #291

Assisted-by: Claude:claude-opus-4-8[1m]
The pack half of #291. Relocation asked `is_nothrow_constructible_v` -
direct-initialization - where the aggregate initialization in `_append`
copy-initializes each element from its source ([dcl.init.aggr]/4.3), which
cannot reach an explicit constructor: an element with an explicit nothrow move
and a throwing copy made `append` promise a `noexcept` the deed cannot keep.
Nothing asked whether relocation was possible at all, so appending to a pack
whose element cannot make the move hard-errored inside the body where a clean
rejection was owed. `as_pack` asked the same question of `pack` itself, where
parenthesized initialization performs no brace elision - false for every
argument list, a permanent under-promise. `invoke_r` promised
direct-initialization of `Ret` and performed a return statement.

`_make_element` now declares its own contract, `_relocatable_element` asks the
element holder for the copy-initialization it performs, `_append` is
constrained on and specified by both questions, and every `append` overload
asks the `_append` it calls. `as_pack` is constrained on and specified by the
braces it performs, and declares its return type. `invoke_r` delegates to the
new `pack_impl::_invoke_r`, asking the same `INVOKE<R>` question `sum::invoke_r`
asks - which also aligns `invoke_r<void>`, accepted by `std::invoke_r` and
`sum` and until now rejected by `pack`.

Closes #291

Assisted-by: Claude:claude-fable-5
`pack` declares no constructor and no special member - `_element`, `pack_impl`
and `pack` itself are aggregates, and everything above rests on that: brace
initialization with elision, the structural type, and special members composed
from the elements' own. A constructor added anywhere in the stack would change
all of it in silence; these assertions fail instead.

Assisted-by: Claude:claude-fable-5
@Bronek Bronek force-pushed the bronek/P11/initialization_questions branch from 68e5e78 to c36d9d7 Compare July 13, 2026 19:45
@augmentcode

augmentcode Bot commented Jul 13, 2026

Copy link
Copy Markdown

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

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@Bronek Bronek merged commit 88badc8 into main Jul 13, 2026
63 checks passed
@sonarqubecloud

Copy link
Copy Markdown

Bronek added a commit that referenced this pull request Jul 13, 2026
Retire #291's tripwire: as_pack asks the braces it performs, so it no longer
under-promises for every argument list, and #305's own section says it better
than the pin did - an lvalue binds rather than moves, and the constraint
follows the same initialization.

Drop our coverage of sum's temporary arm completing, which #305 brings with a
constant-evaluation twin: the same test, said twice, would not have compiled.

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.

sum and pack ask is_[nothrow_]constructible about storage that list-initializes — rejecting what it supports, promising noexcept it cannot keep

1 participant