Skip to content

Expected over references (reordered history, byte-identical replay)#4

Closed
steve-downey wants to merge 16 commits into
mainfrom
expected-over-references-replay
Closed

Expected over references (reordered history, byte-identical replay)#4
steve-downey wants to merge 16 commits into
mainfrom
expected-over-references-replay

Conversation

@steve-downey

Copy link
Copy Markdown
Owner

Re-authored history of the expected<T&, E> reference-support work, ending byte-identical to the tip of expected-over-references (the tree OID matches exactly). This is a cleaned-up replay of that branch, opened to run CI on the reordered commits.

What changed vs. the original branch

The final code/tests/docs are identical; only the history differs. The reorder:

  • Removes the blind alley. The original branch built three separate expected specializations for reference error types (expected<T,E&>, <T&,E&>, <void,E&>) and then deleted them once unexpected<E&> unification was discovered. This replay specializes unexpected<E&> first, so the error-reference axis collapses into the primary/void templates from the start — no throwaway specializations (verified: 0 separate E& classes ever added).
  • Removes the inline→split round trip. All class bodies are authored out-of-line from the start (no = delete("...") churn either — verified 0 occurrences).
  • Folds corrections into the specializations as they are introduced: move-then-deref refstealing correctness, rebinding assignment from unexpected<E&>, the expected<void,E> self-type exclusion, and the explicit expected<void,E> form.

Commits (4 core + 3 tail)

  1. feat: add unexpected<E&> partial specialization (pointer storage)
  2. feat: expected<T,E> stores unexpected<E>, enabling reference error types
  3. feat: expected<void,E> stores unexpected<E>, enabling expected<void,E&>
  4. feat: add expected<T&,E> reference specialization (covers T&,E and T&,E&)
  5. docs: add design/review guides, D4280R0 paper, and plan updates
  6. build: sync copier metadata, presets, coverage, and CI to release state
  7. chore: remove todo scaffolding superseded by the library

Each of the four code commits builds and passes its own tests independently (253 → 327 → 404 → 734 tests as each specialization is introduced). Locally verified green with cmake --workflow --preset gcc-debug (734/734) at every core commit and at the tip.

🤖 Generated with Claude Code

Add a partial specialization unexpected<E&> that stores a pointer instead
of a value, plus the reference_constructs_from_temporary_v trait it needs.
This is the uniform error-storage building block: a union cannot hold a
reference member directly, but it can hold unexpected<E&> (just a pointer),
so every expected<> union can store unexpected<E> for both value and
reference E. Purely additive over the primary unexpected<E> template.
Change the primary expected<T,E> union to hold unexpected<E> instead of a
raw E. Because unexpected<E&> stores a pointer, this makes E an lvalue
reference valid with no extra specialization: expected<T,E&> is just the
primary template. Adds the reference-E converting constructors and the
rebinding operator= from unexpected<E&>; deletes the dangling value-G
cases. Uses the move-then-deref idiom (*std::move(rhs), std::move(x).error())
throughout so converting from a reference-holding expected copies the
referent instead of stealing it. expected<int,int&> is now well-formed, so
its negative test is dropped; the error-reference test group is added.
Rewrite the void specialization as the explicit expected<void,E> form and
store the error as unexpected<E> in its union, so E may be an lvalue
reference by the same unexpected<E&> mechanism as the primary. Adds the
reference-E converting constructor from expected<void,G&>, the rebinding
operator= from unexpected<E&>, the self-type exclusion on the converting
constructor, and move-then-deref on all error accesses. expected<void,int&>
is now well-formed, so its negative test is dropped; the void+error-
reference test group is added.
…,E&)

Add the expected<T&,E> partial specialization: value is a reference,
stored as a pointer, with the error stored as unexpected<E> so E may also
be a reference (this single specialization covers both expected<T&,E> and
expected<T&,E&>). Out-of-line definitions throughout, dangling-temporary
prevention via reference_constructs_from_temporary_v, shallow move-then-
deref conversions, and the full monadic surface. Brings in the reference
value/both-reference test groups, the shared testing/types.hpp, and the
cross-cutting review-corrections regression suite. Removes the todo.test.cpp
scaffolding. Header and tests now match the release state.
@coveralls

coveralls commented Jul 5, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 28761853430

Coverage increased (+6.8%) to 97.017%

Details

  • Coverage increased (+6.8%) from the base build.
  • Patch coverage: 257 of 257 lines across 2 files are fully covered (100%).
  • 19 coverage regressions across 1 file.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

19 previously-covered lines in 1 file lost coverage.

File Lines Losing Coverage Coverage
include/beman/expected/expected.hpp 19 97.12%

Coverage Stats

Coverage Status
Relevant Lines: 704
Covered Lines: 683
Line Coverage: 97.02%
Coverage Strength: 25.96 hits per line

💛 - Coveralls

…f (clang)

clang (through 22) rejects an out-of-line constrained member of a partial
specialization when its requires-clause names a member typedef of the
class: 'out-of-line definition of error_or does not match any declaration
in expected<T&, E>'. The primary template is unaffected; only the
expected<T&, E> partial specialization's error_or triggered it.

Write the two error_or requires-clauses (declaration and definition) as the
underlying std::remove_cv_t<std::remove_reference_t<E>> expression instead
of error_value_type. Semantically identical (error_value_type is that very
alias), but no member-typedef reference for clang to choke on. gcc and
clang-22 (libc++, gnu++20 and gnu++26) both accept it; full gcc-debug suite
still 734/734.

Note: this bug is present in the original expected-over-references tip too
(this branch was byte-identical to it); the same fix applies there.
The reference_converts_from_temporary_v trait was defined (via std:: or the
__reference_converts_from_temporary builtin) but never used — every dangling
check in the library uses reference_constructs_from_temporary_v. Clang 18
provides __reference_constructs_from_temporary but not the converts sibling,
so defining the unused alias broke the build there ('T'/'U' does not refer to
a value) and cascaded into spurious 'out-of-line definition does not match'
errors. Remove the dead trait. No behavioral change; gcc and clang-22 remain
green.

Pre-existing on the original expected-over-references tip as well.
Commit f7c44c7 set GCC 13 as the minimum by relying on the
__reference_constructs_from_temporary builtin (GCC 13+ / Clang 16+) with no
polyfill fallback. gcc 11/12 have neither that builtin nor the C++23
<__cpp_lib_reference_from_temporary> library feature, so they cannot build.
Remove them from the CI matrix to match the stated support floor. MSVC is
left for a separate decision.
The msvc-debug/msvc-release presets inherited _root-config's C++20 default,
where MSVC has neither __cpp_lib_reference_from_temporary (a C++23 library
feature) nor the __reference_constructs_from_temporary builtin, leaving
detail::reference_constructs_from_temporary_v undefined. Override
CMAKE_CXX_STANDARD to 23 for both MSVC presets so std::reference_constructs_
from_temporary_v is picked up. Other presets are unchanged.
The dangling-detection trait relies on __reference_constructs_from_temporary
(builtin) or the C++23 <__cpp_lib_reference_from_temporary> feature, and on
the compiler canonicalizing an out-of-line constrained member's requires-
clause against its in-class declaration. Those hold on GCC 13+ and Clang 19+
but not below:

- Clang 17: lacks the builtin entirely (trait undefined).
- Clang 18: has the builtin but rejects ~15 out-of-line members whose
  requires-clause names the self-type/member-typedef with a spelling that
  differs from the in-class declaration (Clang 22/GCC canonicalize; Clang 18
  does not).
- MSVC: same constraint-matching strictness (C2244), even in C++23 where the
  trait itself is available.

Rather than rewrite every constrained member's declaration to be token-
identical to its definition, set the supported floor. Drop clang 17/18 and
the MSVC build-and-test + preset-test jobs from the matrix (gcc 11/12 were
already removed), and revert the now-moot MSVC C++23 preset bump.

These failures are pre-existing on the original expected-over-references tip;
supporting clang<19 / MSVC would be a separate constraint-normalization pass.
…EADME

Update Supported Platforms to the enforced CI floor (GCC 13-16, Clang 19-22,
AppleClang) and add a 'Compiler floor and unsupported toolchains' subsection
explaining why: the reference_constructs_from_temporary builtin lands in GCC 13
/ Clang 19, and Clang 18 additionally mismatches out-of-line constrained member
definitions. Note that MSVC is unsupported not by design but because we lack
MSVC access to develop and verify the out-of-line-definition workarounds it
(like Clang 18) needs; contributions with MSVC access welcome.
Resolves the tests/beman/expected/CMakeLists.txt conflict between the
std::expected parity test harness (main) and the reference-specialization
work (this branch):

- Keep main's behavioral/beman-only test-source split and the
  beman.expected.tests.expected.std parity executable.
- File the new reference-specialization test sources (expected_ref*.test.cpp,
  expected_void_ref_e.test.cpp, expected_review_corrections.test.cpp) under
  beman_expected_beman_only_tests, since std::expected has no reference
  specializations to parameterize against.
- Restore the local find_package(Catch2)/CMAKE_MODULE_PATH block this branch
  had moved out of the top-level CMakeLists.txt (main never touched that
  region, so the merge silently kept this branch's removal without it).
- Drop todo.test.cpp from the merged source list; this branch already deleted
  the scaffolding it exercised.
- Parameterize tests/beman/expected/testing/types.hpp's from_expected/
  from_unexpected helpers over test_ns (via the new
  beman/expected/test_expected.hpp) instead of hardcoding beman::expected, and
  add the tests/ include root to both test executables so the header can use
  the canonical, namespaced <beman/expected/test_expected.hpp> include instead
  of a same-directory relative one.
- Fix two leftover hardcoded beman::expected::bad_expected_access references
  in expected_monadic.test.cpp that main's parity refactor hadn't reached.

Verified with `make TOOLCHAIN=gcc-15 test`: 1080/1080 tests pass, including
the new std.* parity suite.
Two narrow toolchain issues surfaced in CI on the new .std parity target,
neither a beman::expected behavioral difference:

- expected.test.cpp: three "value() throw carries the error" tests discard
  the [[nodiscard]] return of e.value() before catching the exception it
  throws; cast to void so -Werror builds don't choke on -Wunused-result.
- unexpected.test.cpp: std::unexpected<int> == std::unexpected<long> fails
  to compile on some libc++ versions (clang 19, appleclang c++26) with a
  cross-specialization friend-access error. Guarded the test
  #ifndef BEMAN_EXPECTED_TEST_STD; beman's own unexpected doesn't have this
  problem.

Documented both, plus a third known-but-unfixed issue (a pervasive Catch2
decomposition x std::expected constrained-operator== interaction that trips
"satisfaction of constraint depends on itself" on some libc++ versions,
across potentially dozens of comparison sites in the parameterized suite) in
docs/std-parity.md as a known, non-blocking limitation of affected
toolchains rather than sweeping every call site.
The .std target exists to catch beman::expected behavioral differences from
std::expected, not to chase the standard library's own bugs. libc++ has (at
least) two independent bugs of its own that surface running this suite
against it (see docs/std-parity.md): a cross-specialization friend-access
error in std::unexpected's heterogeneous operator==, and a pervasive
Catch2-decomposition-triggered self-referential constraint check in
std::expected's constrained operator==. Detect libc++ (_LIBCPP_VERSION, via
check_cxx_symbol_exists — covers both explicit -stdlib=libc++ and
AppleClang's implicit default) and skip building the .std target there
entirely; gcc/libstdc++ and clang-with-libstdc++ configurations are
unaffected and keep running it.

Verified the skip takes effect under clang-19 + libc++ (BEMAN_EXPECTED_USING_LIBCXX
is set and beman.expected.tests.expected.std is absent from the ninja target
list), and that make TOOLCHAIN=gcc-15 test still builds and runs the .std
target normally (1079/1079 tests pass).
@steve-downey

Copy link
Copy Markdown
Owner Author

Superseded by bemanproject#61, opened directly against upstream main.

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.

2 participants