Skip to content

Key expected's SMF constraints/noexcept on E, not the exposition-only unexpected<E>#69

Merged
steve-downey merged 2 commits into
bemanproject:mainfrom
steve-downey:smf-constraints-on-e
Jul 12, 2026
Merged

Key expected's SMF constraints/noexcept on E, not the exposition-only unexpected<E>#69
steve-downey merged 2 commits into
bemanproject:mainfrom
steve-downey:smf-constraints-on-e

Conversation

@steve-downey

Copy link
Copy Markdown
Member

Summary

expected<T,E> stores its error as the exposition-only unexpected<E> (D4280) so the reference specializations can share one member type (unexpected<E&> holds a pointer). This PR makes sure that's genuinely observable-only, ABI-preserving scaffolding:

  • Re-keys every special-member Constraints/Mandates/noexcept/triviality condition in expected.hpp (all three specializations) from unexpected<E> to E, and does the same in the just-landed wording (expected-new.tex). An implementation that stores E directly stays trivially conforming — no ABI, layout, or mangling change is forced on anyone.
  • Fixes two divergences from std::expected a beman-vs-std trait-equivalence harness (added here) turned up:
    • the hidden-friend swap was unconstrained, so is_swappable_v<expected<T,E>> was a hard compile error (not a well-formed false) for a move-restricted E
    • the non-trivial copy constructor/assignment were missing their conditional noexcept
  • Declares the trivial-path copy/move constructor unconditionally (no requires-clause), matching libstdc++'s own <expected> idiom, after the equivalence harness caught GCC and Clang disagreeing on is_trivially_copyable_v<expected<int, Immovable>> for a fully-immovable error type. The same trick was tried for assignment and reverted — it silently broke the "move assignment must be deleted when neither side is nothrow-movable" safety guarantee — so assignment operators keep their original two-candidate form; this is documented in the paper rather than forced.
  • Documents both decisions in D4280R0.tex (D12: exposition-only member, ABI stability as a design goal, resolution toward the T/E rendering if any divergence is ever found; D13: expected<T,E> is trivially copyable when T,E are, called out as a drive-by fix over std::expected's non-trivial-by-specification-accident assignment).

Test plan

  • Full suite (1081 tests, including the std::expected parity gate and the new equivalence gate) passes on GCC/libstdc++
  • Full suite passes on Clang/libstdc++
  • pre-commit (clang-format, gersemi, codespell, beman-tidy) clean on all changed files

Re-key the special-member constraints, noexcept specifications, and
triviality conditions of expected<T,E> from the exposition-only held type
unexpected<E> onto E, so that an implementation storing E directly is
trivially conforming and ABI stability for existing implementations is
preserved. The held member remains unexpected<E>; only the observable
properties move to E.

Fixes surfaced by a beman-vs-std trait-equivalence harness:
- constrain the hidden-friend swap so is_swappable_v<expected> is a
  well-formed bool, not a hard error, for a move-restricted E
- give the non-trivial copy constructor and copy assignment the
  conditional noexcept they were missing, matching std::expected

Regression tests: expected_smf_regressions.test.cpp (runs everywhere)
and expected_std_equivalence.test.cpp (full trait parity vs std::expected,
gated off libc++ like the existing std-parity target).
…r D12/D13

Re-key the freshly-landed [expected.object.cons]/[expected.void.cons]
wording's swap/dtor/ctor noexcept and triviality remarks from
unexpected<E> to E in expected-new.tex, matching the header.

Declare the trivial-path copy/move constructor unconditionally
(no requires-clause) in all three specializations, matching
libstdc++'s own <expected> idiom: when neither the trivial nor the
non-trivial candidate's constraint holds (T or E not constructible
at all), this is the sole declaration, implicitly defined as
deleted, and compilers agree on it. Two guarded candidates that both
fail to apply left GCC and Clang disagreeing on whether
expected<int, Immovable> is trivially copyable. Assignment operators
are deliberately left on their original two-candidate form: unlike
construction, a naive unconstrained fallback there risks skipping
the reinit-across-states algorithm's safety guard, confirmed by a
regression (move assignment stayed assignable when it must be
deleted) that appeared and was reverted during this change.

Document both decisions in D4280R0.tex (D12 gets an implementation
note) and skip the one known, pre-existing, toolchain-specific
triviality quirk for a fully-immovable error type in the
equivalence test, with a comment explaining why.

Full suite (1081 tests, including std-parity and the new
equivalence gate) passes on both GCC/libstdc++ and Clang/libstdc++.
@steve-downey
steve-downey merged commit 5ac68a0 into bemanproject:main Jul 12, 2026
77 checks passed
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.

1 participant