From 5c417b9751fce8ff78f3d405c6fb57e8545f9ed5 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Thu, 6 Aug 2026 17:09:17 -0400 Subject: [PATCH 1/7] Docs & metadata: fix stale badges, broken links, citation, and doc drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README.md: Update CI/pre-commit/coverage badge URLs from sandbox-expected to expected - README.md: Fix broken wg21.link/D4280R0 link; point to in-repo papers/D4280R0.tex as draft - papers/mybiblio.bib: Fix stale Downey_beman_expected entry—update URL from expected26 to expected and title from beman.expected26 to beman.expected - docs/human-design-review-guide.md: Resolve contradiction at line 109; rebinding assignment from unexpected is now supported (not construction-only) - docs/std-parity.md: Update stale premise; reference specializations are now implemented - docs/conformance-audit.md: Update row 99; E reference constraint is RELAXED to permit lvalue references per reference extension Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 4 ++-- docs/conformance-audit.md | 2 +- docs/human-design-review-guide.md | 2 +- docs/std-parity.md | 7 +++---- papers/mybiblio.bib | 4 ++-- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ae7e3c3..cb669f3 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,11 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception --> -[![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg)](https://github.com/bemanproject/beman/blob/main/docs/beman_library_maturity_model.md#the-beman-library-maturity-model) ![Continuous Integration Tests](https://github.com/steve-downey/sandbox-expected/actions/workflows/ci_tests.yml/badge.svg) ![Lint Check (pre-commit)](https://github.com/steve-downey/sandbox-expected/actions/workflows/pre-commit-check.yml/badge.svg) [![Coverage](https://coveralls.io/repos/github/steve-downey/sandbox-expected/badge.svg?branch=main)](https://coveralls.io/github/steve-downey/sandbox-expected?branch=main) ![Standard Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp29.svg) +[![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg)](https://github.com/bemanproject/beman/blob/main/docs/beman_library_maturity_model.md#the-beman-library-maturity-model) ![Continuous Integration Tests](https://github.com/steve-downey/expected/actions/workflows/ci_tests.yml/badge.svg) ![Lint Check (pre-commit)](https://github.com/steve-downey/expected/actions/workflows/pre-commit-check.yml/badge.svg) [![Coverage](https://coveralls.io/repos/github/steve-downey/expected/badge.svg?branch=main)](https://coveralls.io/github/steve-downey/expected?branch=main) ![Standard Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp29.svg) `beman.expected` is a C++ library implementing the std::expected specification conforming to [The Beman Standard](https://github.com/bemanproject/beman/blob/main/docs/beman_standard.md). -**Implements**: `std::expected` proposed in [Expected over References (D4280R0)](https://wg21.link/D4280R0). +**Implements**: `std::expected` proposed in [Expected over References (D4280R0, draft)](papers/D4280R0.tex). **Status**: [Under development and not yet ready for production use.](https://github.com/bemanproject/beman/blob/main/docs/beman_library_maturity_model.md#under-development-and-not-yet-ready-for-production-use) diff --git a/docs/conformance-audit.md b/docs/conformance-audit.md index 36a684c..be684f5 100644 --- a/docs/conformance-audit.md +++ b/docs/conformance-audit.md @@ -96,7 +96,7 @@ All four `error()` overloads: **PASS** | T is not unexpect_t | PASS | | | T is not an array | PASS | | | T is not a specialization of unexpected | FIXED (Fix 4) | static_assert added | -| E is not a reference | PASS | | +| E is not a reference | RELAXED | E may be an lvalue reference (reference extension) | | E is not void | PASS | | | E is not an array | PASS | | diff --git a/docs/human-design-review-guide.md b/docs/human-design-review-guide.md index 3abbf6f..a2dbac0 100644 --- a/docs/human-design-review-guide.md +++ b/docs/human-design-review-guide.md @@ -106,7 +106,7 @@ This matches `T* const` (const pointer to non-const T), `std::reference_wrapper` ### What was done -For `expected`, `expected`, and `expected`, construction from `unexpected` is allowed **only when `G` is itself a reference** (i.e. from `unexpected`), and `= delete`d when `G` is a value type. Assignment from `unexpected` is still not offered for reference `E` (construction-only, for now). +For `expected`, `expected`, and `expected`, construction from `unexpected` is allowed **only when `G` is itself a reference** (i.e. from `unexpected`), and `= delete`d when `G` is a value type. Rebinding assignment from `unexpected` is now supported alongside construction, following the same reference-`G`-only rule. ```cpp int err = 42; diff --git a/docs/std-parity.md b/docs/std-parity.md index 8f191fd..cb48e81 100644 --- a/docs/std-parity.md +++ b/docs/std-parity.md @@ -1,9 +1,8 @@ # std::expected Parity -Before adding the reference specializations (`expected` etc., plan -steps 7–10), the behavioral test suite is run against **both** -`beman::expected` and `std::expected` to prove there are no behavioral -differences outside the reference extension. +The reference specializations (`expected` etc.) are now implemented. The +behavioral test suite is run against **both** `beman::expected` and `std::expected` +to confirm there are no behavioral differences outside the reference extension. ## How it works diff --git a/papers/mybiblio.bib b/papers/mybiblio.bib index 4585afa..2e4cea0 100644 --- a/papers/mybiblio.bib +++ b/papers/mybiblio.bib @@ -8,8 +8,8 @@ @misc{D4270R0 @misc{Downey_beman_expected, author = {Downey, Stephen}, -title = {{beman.expected26}}, -howpublished = {\url{https://github.com/bemanproject/expected26}}, +title = {{beman.expected}}, +howpublished = {\url{https://github.com/bemanproject/expected}}, } @misc{The_Beman_Project_beman_optional, From 43a2773db05827d179d33963ef72f39930611b4f Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Thu, 6 Aug 2026 17:10:02 -0400 Subject: [PATCH 2/7] Design paper: fix first/second ordinal; describe guarded delete messages The abstract said this paper delivers the "second" of the two follow-ons P2988R12 named; the motivation correctly said "first" (expected precedes variant in P2988R12's own ordering). Fix the abstract to agree. Rewrite D10: the implementation now gates `= delete("msg")` behind __cpp_deleted_function and falls back to a plain `= delete` where the compiler lacks it, so the library floor stays C++20 rather than moving to C++26. Note in D11 that the reference implementation's feature test macro value is a placeholder pending LWG assignment. Co-Authored-By: Claude Opus 4.8 (1M context) --- papers/D4280R0.tex | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/papers/D4280R0.tex b/papers/D4280R0.tex index 5b52af4..088f36e 100644 --- a/papers/D4280R0.tex +++ b/papers/D4280R0.tex @@ -41,7 +41,7 @@ is shallow --- the binding is fixed, the referent is not. Constructors that would bind a reference to a temporary are deleted, with a diagnostic. This completes reference support across the standard library's principal - fallible sum type, and delivers the second of the two follow-ons that + fallible sum type, and delivers the first of the two follow-ons that \cite{P2988R12} named. \end{abstract} @@ -600,19 +600,24 @@ \section{Shallow conversions must not steal (D9)} Because it is a guarantee and not merely an implementation habit, it belongs in the design. -\section{Deleted functions carry messages (D10)} +\section{Deleted functions carry messages, where the compiler allows it (D10)} -Every deleted operation uses \tcode{= delete("...")} \cite{P2573R2} to say what -is wrong and what to do instead --- for instance, ``\tcode{expected: no -default constructor; T\& cannot be null}.'' This raises the language floor for -the reference specializations to \CppXXVI{}; the primary template does not need -it. For a feature whose whole difficulty is explaining why a given -construction is refused, a good diagnostic is worth the floor. +Every deleted operation is written to say what is wrong and what to do +instead --- for instance, ``\tcode{expected: no default constructor; +T\& cannot be null}.'' Deleted-function messages \cite{P2573R2} are +\CppXXVI{}; this proposal does not raise the library's floor to get them. A +macro emits \tcode{= delete("...")} when the compiler advertises support --- +tested via \tcode{__cpp_deleted_function} --- and falls back to a plain +\tcode{= delete} otherwise. The reference implementation, and this proposal, +stay at \CppXX{}. For a feature whose whole difficulty is explaining why a +given construction is refused, a good diagnostic is worth having when it is +free, but not worth a floor. \section{Feature test macro (D11)} A feature test macro \tcode{__cpp_lib_expected_ref} is proposed, so code can -detect the reference specializations. +detect the reference specializations. The reference implementation defines it +with a placeholder value, pending assignment by LWG on adoption. \section{The held \tcode{unexpected} is exposition-only; the observable behavior is keyed on \tcode{E} (D12)} From 5ef26d4dfc46090d48219a7acc159224ba9bb8c9 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Fri, 7 Aug 2026 10:24:24 -0400 Subject: [PATCH 3/7] Fix: expected<...,E&> copy/move assignment incorrectly disabled for const E& MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The copy/move assignment operator= constraints on expected, expected, and expected keyed off is_copy_assignable_v / is_move_assignable_v directly. For a non-const reference E (e.g. int&) these traits happen to be true (assignment through the reference), but for a const reference E (e.g. const int&) they are false, since you cannot assign through a const reference — even though the actual stored unexpected rebinds via pointer assignment and does not need to assign through E at all. Relax the constraint to `is_reference_v || (is_copy_constructible_v && is_copy_assignable_v)` (and the move analog) everywhere it appears: the primary template, the void specialization, and the T& specialization (both in-class declarations and out-of-line definitions). The assignment bodies already rebind correctly for reference E (unexpected's pointer member is copied/constructed, never assigned through), so no body changes were needed — verified with new tests asserting is_copy_assignable_v / is_move_assignable_v for const-reference E, and runtime tests confirming rebind-not-assign-through behavior. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/beman/expected/expected.hpp | 32 +++++++++---------- .../beman/expected/expected_ref_both.test.cpp | 28 ++++++++++++++++ tests/beman/expected/expected_ref_e.test.cpp | 28 ++++++++++++++++ .../expected/expected_void_ref_e.test.cpp | 28 ++++++++++++++++ 4 files changed, 100 insertions(+), 16 deletions(-) diff --git a/include/beman/expected/expected.hpp b/include/beman/expected/expected.hpp index 6ad625e..482b427 100644 --- a/include/beman/expected/expected.hpp +++ b/include/beman/expected/expected.hpp @@ -309,8 +309,8 @@ class expected { std::is_nothrow_copy_assignable_v && std::is_nothrow_copy_constructible_v && std::is_nothrow_copy_assignable_v) - requires(std::is_copy_constructible_v && std::is_copy_assignable_v && std::is_copy_constructible_v && - std::is_copy_assignable_v && + requires(std::is_copy_constructible_v && std::is_copy_assignable_v && + (std::is_reference_v || (std::is_copy_constructible_v && std::is_copy_assignable_v)) && (std::is_nothrow_move_constructible_v || std::is_nothrow_move_constructible_v) && !(std::is_trivially_copy_constructible_v && std::is_trivially_copy_assignable_v && std::is_trivially_destructible_v && std::is_trivially_copy_constructible_v && @@ -328,8 +328,8 @@ class expected { std::is_nothrow_move_assignable_v && std::is_nothrow_move_constructible_v && std::is_nothrow_move_assignable_v) - requires(std::is_move_constructible_v && std::is_move_assignable_v && std::is_move_constructible_v && - std::is_move_assignable_v && + requires(std::is_move_constructible_v && std::is_move_assignable_v && + (std::is_reference_v || (std::is_move_constructible_v && std::is_move_assignable_v)) && (std::is_nothrow_move_constructible_v || std::is_nothrow_move_constructible_v) && !(std::is_trivially_move_constructible_v && std::is_trivially_move_assignable_v && std::is_trivially_destructible_v && std::is_trivially_move_constructible_v && @@ -715,8 +715,8 @@ template constexpr expected& expected::operator=(const expected& rhs) noexcept( std::is_nothrow_copy_constructible_v && std::is_nothrow_copy_assignable_v && std::is_nothrow_copy_constructible_v && std::is_nothrow_copy_assignable_v) - requires(std::is_copy_constructible_v && std::is_copy_assignable_v && std::is_copy_constructible_v && - std::is_copy_assignable_v && + requires(std::is_copy_constructible_v && std::is_copy_assignable_v && + (std::is_reference_v || (std::is_copy_constructible_v && std::is_copy_assignable_v)) && (std::is_nothrow_move_constructible_v || std::is_nothrow_move_constructible_v) && !(std::is_trivially_copy_constructible_v && std::is_trivially_copy_assignable_v && std::is_trivially_destructible_v && std::is_trivially_copy_constructible_v && @@ -743,8 +743,8 @@ constexpr expected& expected::operator=(expected&& rhs) noexcept(std std::is_nothrow_move_assignable_v && std::is_nothrow_move_constructible_v && std::is_nothrow_move_assignable_v) - requires(std::is_move_constructible_v && std::is_move_assignable_v && std::is_move_constructible_v && - std::is_move_assignable_v && + requires(std::is_move_constructible_v && std::is_move_assignable_v && + (std::is_reference_v || (std::is_move_constructible_v && std::is_move_assignable_v)) && (std::is_nothrow_move_constructible_v || std::is_nothrow_move_constructible_v) && !(std::is_trivially_move_constructible_v && std::is_trivially_move_assignable_v && std::is_trivially_destructible_v && std::is_trivially_move_constructible_v && @@ -1543,7 +1543,7 @@ class expected { // Copy assignment (non-trivial path) constexpr expected& operator=(const expected& rhs) noexcept(std::is_nothrow_copy_constructible_v && std::is_nothrow_copy_assignable_v) - requires(std::is_copy_constructible_v && std::is_copy_assignable_v && + requires((std::is_reference_v || (std::is_copy_constructible_v && std::is_copy_assignable_v)) && !(std::is_trivially_copy_constructible_v && std::is_trivially_copy_assignable_v && std::is_trivially_destructible_v)); @@ -1556,7 +1556,7 @@ class expected { // Move assignment (non-trivial path) constexpr expected& operator=(expected&& rhs) noexcept(std::is_nothrow_move_constructible_v && std::is_nothrow_move_assignable_v) - requires(std::is_move_constructible_v && std::is_move_assignable_v && + requires((std::is_reference_v || (std::is_move_constructible_v && std::is_move_assignable_v)) && !(std::is_trivially_move_constructible_v && std::is_trivially_move_assignable_v && std::is_trivially_destructible_v)); @@ -1835,7 +1835,7 @@ template constexpr expected& expected::operator=(const expected& rhs) noexcept(std::is_nothrow_copy_constructible_v && std::is_nothrow_copy_assignable_v) - requires(std::is_copy_constructible_v && std::is_copy_assignable_v && + requires((std::is_reference_v || (std::is_copy_constructible_v && std::is_copy_assignable_v)) && !(std::is_trivially_copy_constructible_v && std::is_trivially_copy_assignable_v && std::is_trivially_destructible_v)) { @@ -1857,7 +1857,7 @@ template constexpr expected& expected::operator=(expected&& rhs) noexcept(std::is_nothrow_move_constructible_v && std::is_nothrow_move_assignable_v) - requires(std::is_move_constructible_v && std::is_move_assignable_v && + requires((std::is_reference_v || (std::is_move_constructible_v && std::is_move_assignable_v)) && !(std::is_trivially_move_constructible_v && std::is_trivially_move_assignable_v && std::is_trivially_destructible_v)) { @@ -2489,7 +2489,7 @@ class expected { // Copy assignment (non-trivial path) constexpr expected& operator=(const expected& rhs) noexcept(std::is_nothrow_copy_constructible_v && std::is_nothrow_copy_assignable_v) - requires(std::is_copy_constructible_v && std::is_copy_assignable_v && + requires((std::is_reference_v || (std::is_copy_constructible_v && std::is_copy_assignable_v)) && !(std::is_trivially_copy_constructible_v && std::is_trivially_copy_assignable_v && std::is_trivially_destructible_v)); @@ -2502,7 +2502,7 @@ class expected { // Move assignment (non-trivial path) constexpr expected& operator=(expected&& rhs) noexcept(std::is_nothrow_move_constructible_v && std::is_nothrow_move_assignable_v) - requires(std::is_move_constructible_v && std::is_move_assignable_v && + requires((std::is_reference_v || (std::is_move_constructible_v && std::is_move_assignable_v)) && !(std::is_trivially_move_constructible_v && std::is_trivially_move_assignable_v && std::is_trivially_destructible_v)); @@ -2831,7 +2831,7 @@ template constexpr expected& expected::operator=(const expected& rhs) noexcept(std::is_nothrow_copy_constructible_v && std::is_nothrow_copy_assignable_v) - requires(std::is_copy_constructible_v && std::is_copy_assignable_v && + requires((std::is_reference_v || (std::is_copy_constructible_v && std::is_copy_assignable_v)) && !(std::is_trivially_copy_constructible_v && std::is_trivially_copy_assignable_v && std::is_trivially_destructible_v)) { @@ -2854,7 +2854,7 @@ template constexpr expected& expected::operator=(expected&& rhs) noexcept(std::is_nothrow_move_constructible_v && std::is_nothrow_move_assignable_v) - requires(std::is_move_constructible_v && std::is_move_assignable_v && + requires((std::is_reference_v || (std::is_move_constructible_v && std::is_move_assignable_v)) && !(std::is_trivially_move_constructible_v && std::is_trivially_move_assignable_v && std::is_trivially_destructible_v)) { diff --git a/tests/beman/expected/expected_ref_both.test.cpp b/tests/beman/expected/expected_ref_both.test.cpp index 3f58da9..7d66cb1 100644 --- a/tests/beman/expected/expected_ref_both.test.cpp +++ b/tests/beman/expected/expected_ref_both.test.cpp @@ -34,6 +34,12 @@ static_assert(std::is_trivially_copy_assignable_v>); static_assert(std::is_trivially_move_assignable_v>); static_assert(std::is_trivially_destructible_v>); +// Finding 1: copy/move assignment must be available for const-reference E, where E +// itself is not assignable (is_copy_assignable_v is false) but the +// stored unexpected rebinds via pointer assignment. +static_assert(std::is_copy_assignable_v>); +static_assert(std::is_move_assignable_v>); + // operator-> returns T* (shallow const) static_assert(std::is_same_v>().operator->()), int*>); static_assert(std::is_same_v>().operator->()), int*>); @@ -196,6 +202,28 @@ TEST_CASE("expected: transition from value to error via copy assignment", CHECK(&a.error() == &err); } +TEST_CASE("expected: copy assignment rebinds error, does not assign through", + "[expected_ref_both]") { + int e1 = 1, e2 = 2; + expected a(unexpect, e1); + expected b(unexpect, e2); + a = b; + REQUIRE(!a.has_value()); + CHECK(&a.error() == &e2); + CHECK(e1 == 1); +} + +TEST_CASE("expected: move assignment rebinds error, does not assign through", + "[expected_ref_both]") { + int e1 = 1, e2 = 2; + expected a(unexpect, e1); + expected b(unexpect, e2); + a = std::move(b); + REQUIRE(!a.has_value()); + CHECK(&a.error() == &e2); + CHECK(e1 == 1); +} + // Safe alternative to e = unexpected(err): move-assign from a named expected. // No operator=(unexpected) exists for expected — it would bind E& // to temporary storage creating a dangling reference. diff --git a/tests/beman/expected/expected_ref_e.test.cpp b/tests/beman/expected/expected_ref_e.test.cpp index d5331c0..dec4377 100644 --- a/tests/beman/expected/expected_ref_e.test.cpp +++ b/tests/beman/expected/expected_ref_e.test.cpp @@ -37,6 +37,14 @@ static_assert(std::is_same_v>(). static_assert(std::is_copy_constructible_v>); static_assert(std::is_move_constructible_v>); +// Finding 1: copy/move assignment must be available for reference E, including +// const-reference E, where E itself is not assignable (is_copy_assignable_v is false) but the stored unexpected rebinds via pointer assignment. +static_assert(std::is_copy_assignable_v>); +static_assert(std::is_move_assignable_v>); +static_assert(std::is_copy_assignable_v>); +static_assert(std::is_move_assignable_v>); + // Triviality: when T is trivial, copy/move/assign/destroy should be trivial static_assert(std::is_trivially_copy_constructible_v>); static_assert(std::is_trivially_move_constructible_v>); @@ -135,6 +143,26 @@ TEST_CASE("expected: rebind does NOT assign through error reference", "[ex CHECK(a.error() == 20); } +TEST_CASE("expected: copy assignment rebinds, does not assign through", "[expected_ref_e]") { + int a = 1, b = 2; + expected e(unexpect, a); + expected f(unexpect, b); + e = f; + REQUIRE(!e.has_value()); + CHECK(&e.error() == &b); // rebound to f's referent + CHECK(a == 1); // a unchanged — no assign-through +} + +TEST_CASE("expected: move assignment rebinds, does not assign through", "[expected_ref_e]") { + int a = 1, b = 2; + expected e(unexpect, a); + expected f(unexpect, b); + e = std::move(f); + REQUIRE(!e.has_value()); + CHECK(&e.error() == &b); + CHECK(a == 1); +} + TEST_CASE("expected: assign value when in error state", "[expected_ref_e]") { int err = 5; expected e(unexpect, err); diff --git a/tests/beman/expected/expected_void_ref_e.test.cpp b/tests/beman/expected/expected_void_ref_e.test.cpp index 578bd3c..b290f01 100644 --- a/tests/beman/expected/expected_void_ref_e.test.cpp +++ b/tests/beman/expected/expected_void_ref_e.test.cpp @@ -30,6 +30,14 @@ static_assert(std::is_void_v>())>); // absence of operator-> and value_or tested by _fail.cpp negative compile tests +// Finding 1: copy/move assignment must be available for reference E, including +// const-reference E, where E itself is not assignable (is_copy_assignable_v is false) but the stored unexpected rebinds via pointer assignment. +static_assert(std::is_copy_assignable_v>); +static_assert(std::is_move_assignable_v>); +static_assert(std::is_copy_assignable_v>); +static_assert(std::is_move_assignable_v>); + // --------------------------------------------------------------------------- // Construction // --------------------------------------------------------------------------- @@ -122,6 +130,26 @@ TEST_CASE("expected: assign error state to value state", "[expected_voi CHECK(e.has_value()); } +TEST_CASE("expected: copy assignment rebinds, does not assign through", "[expected_void_ref_e]") { + int a = 1, b = 2; + expected e(unexpect, a); + expected f(unexpect, b); + e = f; + REQUIRE(!e.has_value()); + CHECK(&e.error() == &b); + CHECK(a == 1); +} + +TEST_CASE("expected: move assignment rebinds, does not assign through", "[expected_void_ref_e]") { + int a = 1, b = 2; + expected e(unexpect, a); + expected f(unexpect, b); + e = std::move(f); + REQUIRE(!e.has_value()); + CHECK(&e.error() == &b); + CHECK(a == 1); +} + // --------------------------------------------------------------------------- // Shallow const on error // --------------------------------------------------------------------------- From 2f5a05fad773048cbf399049a19f886e9707e4e0 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Fri, 7 Aug 2026 10:30:28 -0400 Subject: [PATCH 4/7] Fix: expected unsafe/inconsistent converting constructor from expected MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two general void converting constructors (for value-typed source error G) were not gated on !is_reference_v, so is_constructible_v incorrectly reported true for constructing expected from both const expected& and expected&&: - The lvalue form would bind E& into the source's own owned error storage, dangling once the (possibly temporary) source is destroyed. - The rvalue form hard-errored inside the constructor body: it forwards into unexpected's constructor, which deletes the overload once it detects the reference would bind to a materialized temporary — a contradiction with the outer is_constructible_v check, which doesn't see that far. Add `!std::is_reference_v` to both general converting constructors (in-class and out-of-line, copy and move) so they only apply to value E, matching the analogous primary-template and T& constraints. The dedicated reference-E path (expected(const expected&)) was also incomplete: it lacked a `!reference_constructs_from_temporary_v` guard (present on the equivalent unexpected reference constructor, but missing here) and had no rvalue overload at all, so std::move'd sources fell through to the (now correctly excluded) general path with no replacement. Added the missing expected(expected&&) overload and the dangling guard to both overloads. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/beman/expected/expected.hpp | 38 +++++++++++++--- .../expected/expected_void_ref_e.test.cpp | 43 +++++++++++++++++++ 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/include/beman/expected/expected.hpp b/include/beman/expected/expected.hpp index 482b427..4025626 100644 --- a/include/beman/expected/expected.hpp +++ b/include/beman/expected/expected.hpp @@ -1443,7 +1443,8 @@ class expected { // unexpected's constructibility from this very class, which some standard library // implementations of reference_constructs_from_temporary_v resolve as a circular constraint. template - requires(std::is_void_v && !std::is_same_v && std::is_constructible_v && + requires(std::is_void_v && !std::is_reference_v && !std::is_same_v && + std::is_constructible_v && !std::is_constructible_v, expected&> && !std::is_constructible_v, expected &&> && !std::is_constructible_v, const expected&> && @@ -1451,7 +1452,8 @@ class expected { constexpr explicit(!std::is_convertible_v) expected(const expected& rhs); template - requires(std::is_void_v && !std::is_same_v && std::is_constructible_v && + requires(std::is_void_v && !std::is_reference_v && !std::is_same_v && + std::is_constructible_v && !std::is_constructible_v, expected&> && !std::is_constructible_v, expected &&> && !std::is_constructible_v, const expected&> && @@ -1514,11 +1516,21 @@ class expected { requires std::is_constructible_v&, Args...> constexpr explicit expected(unexpect_t, std::initializer_list il, Args&&... args); - // Converting constructor from expected — reference-E path only + // Converting constructor from expected — reference-E path only. G is itself a + // reference to an external object, so binding E& to it cannot dangle regardless of the + // source's value category, provided the reference conversion itself does not materialize a + // temporary (e.g. a base-from-derived or qualification conversion is fine; a user-defined + // conversion that returns by value is not). Mirrors the unexpected reference-E path above. template - requires(std::is_reference_v && std::is_convertible_v) + requires(std::is_reference_v && std::is_convertible_v && + !detail::reference_constructs_from_temporary_v) constexpr explicit(!std::is_convertible_v) expected(const expected& rhs); + template + requires(std::is_reference_v && std::is_convertible_v && + !detail::reference_constructs_from_temporary_v) + constexpr explicit(!std::is_convertible_v) expected(expected&& rhs); + // ------------------------------------------------------------------------- // [expected.void.dtor] Destructor // ------------------------------------------------------------------------- @@ -1741,7 +1753,8 @@ constexpr expected::expected(expected&& rhs) noexcept(std::is_nothrow_m template template - requires(std::is_void_v && !std::is_same_v && std::is_constructible_v && + requires(std::is_void_v && !std::is_reference_v && !std::is_same_v && + std::is_constructible_v && !std::is_constructible_v, expected&> && !std::is_constructible_v, expected &&> && !std::is_constructible_v, const expected&> && @@ -1753,7 +1766,8 @@ constexpr expected::expected(const expected& rhs) : has_val_(rhs. template template - requires(std::is_void_v && !std::is_same_v && std::is_constructible_v && + requires(std::is_void_v && !std::is_reference_v && !std::is_same_v && + std::is_constructible_v && !std::is_constructible_v, expected&> && !std::is_constructible_v, expected &&> && !std::is_constructible_v, const expected&> && @@ -1809,12 +1823,22 @@ constexpr expected::expected(unexpect_t, std::initializer_list il, A template template - requires(std::is_reference_v && std::is_convertible_v) + requires(std::is_reference_v && std::is_convertible_v && + !detail::reference_constructs_from_temporary_v) constexpr expected::expected(const expected& rhs) : has_val_(rhs.has_value()) { if (!has_val_) std::construct_at(std::addressof(unex_), rhs.error()); } +template +template + requires(std::is_reference_v && std::is_convertible_v && + !detail::reference_constructs_from_temporary_v) +constexpr expected::expected(expected&& rhs) : has_val_(rhs.has_value()) { + if (!has_val_) + std::construct_at(std::addressof(unex_), rhs.error()); +} + // ============================================================================= // [expected.void.dtor] Out-of-line destructor // ============================================================================= diff --git a/tests/beman/expected/expected_void_ref_e.test.cpp b/tests/beman/expected/expected_void_ref_e.test.cpp index b290f01..91965ab 100644 --- a/tests/beman/expected/expected_void_ref_e.test.cpp +++ b/tests/beman/expected/expected_void_ref_e.test.cpp @@ -38,6 +38,23 @@ static_assert(std::is_move_assignable_v>); static_assert(std::is_copy_assignable_v>); static_assert(std::is_move_assignable_v>); +// Finding 2: the general (value-G) void converting constructors must be gated on +// !is_reference_v — for reference E they are unsound: the lvalue form would bind into +// the source's owned error (dangling once the source is gone), and the rvalue form hard-errors +// by selecting a deleted unexpected constructor deep in the body instead of being excluded +// from overload resolution. is_constructible_v must report false for both, matching reality. +static_assert(!std::is_constructible_v, const expected&>); +static_assert(!std::is_constructible_v, expected&&>); +static_assert(!std::is_constructible_v, const expected&>); +static_assert(!std::is_constructible_v, expected&&>); + +// The dedicated reference-E path (source error type is itself a reference) remains available, +// for both lvalue and rvalue sources. +static_assert(std::is_constructible_v, const expected&>); +static_assert(std::is_constructible_v, expected&&>); +static_assert(std::is_constructible_v, const expected&>); +static_assert(std::is_constructible_v, expected&&>); + // --------------------------------------------------------------------------- // Construction // --------------------------------------------------------------------------- @@ -92,6 +109,32 @@ TEST_CASE("expected: convert from expected", "[expected_void_ CHECK(&dst.error() == &err); } +// Finding 2: the rvalue overload of the reference-E converting constructor was missing; +// only the const& form existed. Verify the && form works and still binds the external +// referent (never dangles — G is itself a reference to the caller's object). +TEST_CASE("expected: convert from expected&& binds external referent", + "[expected_void_ref_e]") { + int err = 9; + expected src(unexpect, err); + expected dst(std::move(src)); + REQUIRE(!dst.has_value()); + CHECK(&dst.error() == &err); +} + +TEST_CASE("expected: convert from expected (lvalue and rvalue)", + "[expected_void_ref_e]") { + int err = 11; + expected src1(unexpect, err); + expected dst1(src1); + REQUIRE(!dst1.has_value()); + CHECK(&dst1.error() == &err); + + expected src2(unexpect, err); + expected dst2(std::move(src2)); + REQUIRE(!dst2.has_value()); + CHECK(&dst2.error() == &err); +} + // --------------------------------------------------------------------------- // Error rebind semantics on assignment // --------------------------------------------------------------------------- From b4823406c49c228385ce1c51d71a9c20384e2b83 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Fri, 7 Aug 2026 10:33:10 -0400 Subject: [PATCH 5/7] Standardese: make reference-E wording coherent; fix typo and undefined ref The added paragraph documenting that the error is held as unexpected (so E may be an lvalue reference) wasn't matched by the special member function wording: copy/move constructor and copy/move assignment Remarks/Constraints for expected and expected still gated on raw is_copy_constructible_v/is_copy_assignable_v/etc., which can spuriously delete these operations (or fail to describe rebind semantics) when E is a reference and the referent is const. Add \added carve-outs for is_reference_v and clarifying \effects paragraphs describing rebind-not-assign-through semantics. Also fix a stray extra '>' in an is_constructible_v Constraints clause, and replace an unresolved \ref{optional.ctor} cross- reference (optional.ctor is not a label in this document) with literal text, which was the source of an undefined-reference warning from latexmk. Co-Authored-By: Claude Opus 4.8 (1M context) --- papers/expected-new.tex | 68 ++++++++++++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 14 deletions(-) diff --git a/papers/expected-new.tex b/papers/expected-new.tex index fbd2f11..20f0290 100644 --- a/papers/expected-new.tex +++ b/papers/expected-new.tex @@ -659,7 +659,7 @@ \pnum The exposition-only variable template \exposid{converts-from-any-cvref} -defined in \ref{optional.ctor} +defined in [optional.ctor] is used by some constructors for \tcode{expected}. \indexlibraryctor{expected}% @@ -712,7 +712,7 @@ \item \tcode{is_copy_constructible_v} is \tcode{true} and \item -\tcode{is_copy_constructible_v} is \tcode{true}. +\tcode{is_copy_constructible_v} is \tcode{true}\added{ or \tcode{is_reference_v} is \tcode{true}}. \end{itemize} \pnum @@ -737,7 +737,7 @@ \item \tcode{is_move_constructible_v} is \tcode{true} and \item -\tcode{is_move_constructible_v} is \tcode{true}. +\tcode{is_move_constructible_v} is \tcode{true}\added{ or \tcode{is_reference_v} is \tcode{true}}. \end{itemize} \pnum @@ -1076,6 +1076,14 @@ Then, if no exception was thrown, equivalent to: \tcode{\exposid{has_val} = rhs.has_value(); return *this;} +\begin{addedblock} +\pnum +When \tcode{E} is an lvalue reference type, each of the cases above that +initializes or assigns \exposid{unex} rebinds it: \exposid{unex} comes to +refer to the same object as \tcode{rhs}'s error. No previously or +subsequently referenced object is assigned through. +\end{addedblock} + \pnum \returns \tcode{*this}. @@ -1089,9 +1097,9 @@ \item \tcode{is_copy_constructible_v} is \tcode{true} and \item -\tcode{is_copy_assignable_v} is \tcode{true} and +\tcode{is_copy_assignable_v} is \tcode{true}\added{ or \tcode{is_reference_v} is \tcode{true}} and \item -\tcode{is_copy_constructible_v} is \tcode{true} and +\tcode{is_copy_constructible_v} is \tcode{true}\added{ or \tcode{is_reference_v} is \tcode{true}} and \item \tcode{is_nothrow_move_constructible_v || is_nothrow_move_constructible_v} is \tcode{true}. @@ -1123,9 +1131,9 @@ \item \tcode{is_move_assignable_v} is \tcode{true} and \item -\tcode{is_move_constructible_v} is \tcode{true} and +\tcode{is_move_constructible_v} is \tcode{true}\added{ or \tcode{is_reference_v} is \tcode{true}} and \item -\tcode{is_move_assignable_v} is \tcode{true} and +\tcode{is_move_assignable_v} is \tcode{true}\added{ or \tcode{is_reference_v} is \tcode{true}} and \item \tcode{is_nothrow_move_constructible_v || is_nothrow_move_constructible_v} is \tcode{true}. @@ -1153,6 +1161,14 @@ Then, if no exception was thrown, equivalent to: \tcode{has_val = rhs.has_value(); return *this;} +\begin{addedblock} +\pnum +When \tcode{E} is an lvalue reference type, each of the cases above that +initializes or assigns \exposid{unex} rebinds it: \exposid{unex} comes to +refer to the same object as \tcode{rhs}'s error. No previously or +subsequently referenced object is assigned through. +\end{addedblock} + \pnum \returns \tcode{*this}. @@ -2101,7 +2117,7 @@ \pnum \remarks This constructor is defined as deleted -unless \tcode{is_copy_constructible_v} is \tcode{true}. +unless \tcode{is_copy_constructible_v} is \tcode{true}\added{ or \tcode{is_reference_v} is \tcode{true}}. \pnum This constructor is trivial @@ -2116,7 +2132,7 @@ \begin{itemdescr} \pnum \constraints -\tcode{is_move_constructible_v} is \tcode{true}. +\tcode{is_move_constructible_v} is \tcode{true}\added{ or \tcode{is_reference_v} is \tcode{true}}. \pnum \effects @@ -2321,6 +2337,14 @@ Otherwise, equivalent to \tcode{\exposid{unex} = rhs.\exposid{unex}}. \end{itemize} +\begin{addedblock} +\pnum +When \tcode{E} is an lvalue reference type, each of the cases above that +initializes or assigns \exposid{unex} rebinds it: \exposid{unex} comes to +refer to the same object as \tcode{rhs}'s error. No previously or +subsequently referenced object is assigned through. +\end{addedblock} + \pnum \returns \tcode{*this}. @@ -2328,8 +2352,12 @@ \pnum \remarks This operator is defined as deleted unless -\tcode{is_copy_assignable_v} is \tcode{true} and -\tcode{is_copy_constructible_v} is \tcode{true}. +\begin{itemize} +\item +\tcode{is_copy_assignable_v} is \tcode{true}\added{ or \tcode{is_reference_v} is \tcode{true}} and +\item +\tcode{is_copy_constructible_v} is \tcode{true}\added{ or \tcode{is_reference_v} is \tcode{true}}. +\end{itemize} \pnum This operator is trivial if @@ -2347,8 +2375,12 @@ \begin{itemdescr} \pnum \constraints -\tcode{is_move_constructible_v} is \tcode{true} and -\tcode{is_move_assignable_v} is \tcode{true}. +\begin{itemize} +\item +\tcode{is_move_constructible_v} is \tcode{true}\added{ or \tcode{is_reference_v} is \tcode{true}} and +\item +\tcode{is_move_assignable_v} is \tcode{true}\added{ or \tcode{is_reference_v} is \tcode{true}}. +\end{itemize} \pnum \effects @@ -2368,6 +2400,14 @@ Otherwise, equivalent to \tcode{\exposid{unex} = std::move(rhs.\exposid{unex})}. \end{itemize} +\begin{addedblock} +\pnum +When \tcode{E} is an lvalue reference type, each of the cases above that +initializes or assigns \exposid{unex} rebinds it: \exposid{unex} comes to +refer to the same object as \tcode{rhs}'s error. No previously or +subsequently referenced object is assigned through. +\end{addedblock} + \pnum \returns \tcode{*this}. @@ -2631,7 +2671,7 @@ \pnum \constraints -\tcode{is_constructible_v>} is \tcode{true}. +\tcode{is_constructible_v} is \tcode{true}. \pnum \mandates From 8b82470d65308db37dee98c50e3bf91a7e45c19d Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Fri, 7 Aug 2026 13:35:24 -0400 Subject: [PATCH 6/7] Add guarded delete-with-message macro and __cpp_lib_expected_ref (no floor bump) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finding 4: introduce BEMAN_EXPECTED_DELETE_MSG(msg) in unexpected.hpp, which expands to C++26's `= delete("msg")` (P2573) when __cpp_deleted_function is available, and to plain `= delete` otherwise — no CMake/README floor change. Replace every `= delete;` on a deleted special member / constructor / assignment / value_or in unexpected.hpp and expected.hpp (reference-related dangling guards, expected's missing default constructor, etc.) with the macro, each message naming the reason and the safe alternative. Verified by grep that no plain `= delete` sites remain, and that the fallback path (active under gcc 13, which does not define __cpp_deleted_function) still compiles and keeps every affected overload deleted. Finding 5: define __cpp_lib_expected_ref (placeholder value, pending WG21 assignment) in expected.hpp as the feature-test macro for the reference-E / reference-T extensions (expected, expected, expected). Tests: expected_ref_e.test.cpp now asserts __cpp_lib_expected_ref is defined and positive, and that two representative deleted operations (unexpected's dangling-temporary constructor, expected's default constructor) stay deleted regardless of which macro expansion is active. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/beman/expected/expected.hpp | 74 ++++++++++++++------ include/beman/expected/unexpected.hpp | 18 ++++- tests/beman/expected/expected_ref_e.test.cpp | 21 ++++++ 3 files changed, 88 insertions(+), 25 deletions(-) diff --git a/include/beman/expected/expected.hpp b/include/beman/expected/expected.hpp index 4025626..b90309c 100644 --- a/include/beman/expected/expected.hpp +++ b/include/beman/expected/expected.hpp @@ -23,6 +23,13 @@ #define BEMAN_EXPECTED_TRAP() std::abort() #endif +// Feature-test macro for the reference-E / reference-T extensions (expected, +// expected, expected) implemented by this header. Not yet a WG21-assigned +// macro; the value below is a placeholder pending standardization. +#ifndef __cpp_lib_expected_ref + #define __cpp_lib_expected_ref 202608L // placeholder value pending WG21 assignment +#endif + /*** 22.8.2 Header synopsis[expected.syn] @@ -244,11 +251,13 @@ class expected { // an unexpected holding an external object, instead. template requires(std::is_reference_v && !std::is_reference_v) - constexpr expected(const unexpected&) = delete; + constexpr expected(const unexpected&) = BEMAN_EXPECTED_DELETE_MSG( + "expected: cannot construct from unexpected; the value would dangle — use unexpected"); template requires(std::is_reference_v && !std::is_reference_v) - constexpr expected(unexpected&&) = delete; + constexpr expected(unexpected&&) = BEMAN_EXPECTED_DELETE_MSG( + "expected: cannot construct from unexpected; the value would dangle — use unexpected"); // In-place constructor for value template @@ -268,14 +277,16 @@ class expected { // Deleted: single argument would bind E& to a temporary — dangling prevention template requires(detail::unexpect_dangles_v) - constexpr expected(unexpect_t, Args&&...) = delete; + constexpr expected(unexpect_t, Args&&...) = BEMAN_EXPECTED_DELETE_MSG( + "expected: unexpect argument would bind a temporary that dangles; pass an lvalue reference"); // Deleted catch-all: reference E, argument neither constructible nor a dangling case // (e.g. binding a non-const E& from a const lvalue). template requires(std::is_reference_v && !std::is_constructible_v && !detail::unexpect_dangles_v) - constexpr expected(unexpect_t, Args&&...) = delete; + constexpr expected(unexpect_t, Args&&...) = BEMAN_EXPECTED_DELETE_MSG( + "expected: no viable conversion from the given argument(s) to E&"); // In-place constructor for error with initializer_list template @@ -373,11 +384,13 @@ class expected { // Deleted for reference E with value G: would rebind E& to unexpected's temporary storage. template requires(std::is_reference_v && !std::is_reference_v) - constexpr expected& operator=(const unexpected&) = delete; + constexpr expected& operator=(const unexpected&) = BEMAN_EXPECTED_DELETE_MSG( + "expected: cannot assign from unexpected; the value would dangle — use unexpected"); template requires(std::is_reference_v && !std::is_reference_v) - constexpr expected& operator=(unexpected&&) = delete; + constexpr expected& operator=(unexpected&&) = BEMAN_EXPECTED_DELETE_MSG( + "expected: cannot assign from unexpected; the value would dangle — use unexpected"); // Emplace: destroy current value/error, construct value in-place template @@ -1485,11 +1498,13 @@ class expected { // binding E& to it would dangle once the source is destroyed. Use (unexpect, lvalue) instead. template requires(std::is_reference_v && !std::is_reference_v) - constexpr expected(const unexpected&) = delete; + constexpr expected(const unexpected&) = BEMAN_EXPECTED_DELETE_MSG( + "expected: cannot construct from unexpected; the value would dangle — use unexpected"); template requires(std::is_reference_v && !std::is_reference_v) - constexpr expected(unexpected&&) = delete; + constexpr expected(unexpected&&) = BEMAN_EXPECTED_DELETE_MSG( + "expected: cannot construct from unexpected; the value would dangle — use unexpected"); // In-place constructor for value (no args, just marks has-value) constexpr explicit expected(std::in_place_t) noexcept; @@ -1502,14 +1517,16 @@ class expected { // Deleted: single argument would bind E& to a temporary — dangling prevention template requires(detail::unexpect_dangles_v) - constexpr expected(unexpect_t, Args&&...) = delete; + constexpr expected(unexpect_t, Args&&...) = BEMAN_EXPECTED_DELETE_MSG( + "expected: unexpect argument would bind a temporary that dangles; pass an lvalue reference"); // Deleted catch-all: reference E, argument neither constructible nor a dangling case // (e.g. binding a non-const E& from a const lvalue). template requires(std::is_reference_v && !std::is_constructible_v && !detail::unexpect_dangles_v) - constexpr expected(unexpect_t, Args&&...) = delete; + constexpr expected(unexpect_t, Args&&...) = BEMAN_EXPECTED_DELETE_MSG( + "expected: no viable conversion from the given argument(s) to E&"); // In-place constructor for error with initializer_list template @@ -1595,11 +1612,13 @@ class expected { // Deleted for reference E with value G: would bind E& to storage inside the temporary unexpected. template requires(std::is_reference_v && !std::is_reference_v) - constexpr expected& operator=(const unexpected&) = delete; + constexpr expected& operator=(const unexpected&) = BEMAN_EXPECTED_DELETE_MSG( + "expected: cannot assign from unexpected; the value would dangle — use unexpected"); template requires(std::is_reference_v && !std::is_reference_v) - constexpr expected& operator=(unexpected&&) = delete; + constexpr expected& operator=(unexpected&&) = BEMAN_EXPECTED_DELETE_MSG( + "expected: cannot assign from unexpected; the value would dangle — use unexpected"); constexpr void emplace() noexcept; @@ -1649,7 +1668,9 @@ class expected { // for value E, no value_or overload is declared at all (there is nothing to delete against). template requires std::is_reference_v - constexpr void value_or(U&&) const = delete; + constexpr void value_or(U&&) const = + BEMAN_EXPECTED_DELETE_MSG("expected: value_or is not defined for void value_type; there is no " + "value to fall back from — use has_value()/error()"); // ------------------------------------------------------------------------- // [expected.void.monadic] Monadic operations @@ -2371,7 +2392,7 @@ class expected { // Constructors // ------------------------------------------------------------------------- - expected() = delete; + expected() = BEMAN_EXPECTED_DELETE_MSG("expected: no default constructor; T& cannot be null"); // Copy constructor (trivial path). Unconstrained; see the primary // template's copy constructor for why. @@ -2390,7 +2411,9 @@ class expected { // Deleted: no in-place value constructor — T& cannot be constructed in-place template - constexpr expected(std::in_place_t, Args&&...) = delete; + constexpr expected(std::in_place_t, Args&&...) = + BEMAN_EXPECTED_DELETE_MSG("expected: no in-place value constructor; T& cannot be constructed " + "in-place — pass a U convertible to T&"); // Value constructor — takes U that can bind to T& template @@ -2407,7 +2430,8 @@ class expected { // Deleted: binding a temporary to T& creates a dangling reference template requires(detail::reference_constructs_from_temporary_v) - constexpr expected(U&&) = delete; + constexpr expected(U&&) = BEMAN_EXPECTED_DELETE_MSG( + "expected: argument would bind a temporary that dangles; pass an lvalue reference"); // Converting constructor from expected (copy) — value-E path template @@ -2462,11 +2486,13 @@ class expected { // an unexpected holding an external object, instead. template requires(std::is_reference_v && !std::is_reference_v) - constexpr expected(const unexpected&) = delete; + constexpr expected(const unexpected&) = BEMAN_EXPECTED_DELETE_MSG( + "expected: cannot construct from unexpected; the value would dangle — use unexpected"); template requires(std::is_reference_v && !std::is_reference_v) - constexpr expected(unexpected&&) = delete; + constexpr expected(unexpected&&) = BEMAN_EXPECTED_DELETE_MSG( + "expected: cannot construct from unexpected; the value would dangle — use unexpected"); // In-place constructor for error template @@ -2476,13 +2502,15 @@ class expected { // Deleted: single argument would bind E& to a temporary — dangling prevention template requires(detail::unexpect_dangles_v) - constexpr expected(unexpect_t, Args&&...) = delete; + constexpr expected(unexpect_t, Args&&...) = BEMAN_EXPECTED_DELETE_MSG( + "expected: unexpect argument would bind a temporary that dangles; pass an lvalue reference"); // Deleted catch-all: reference E, argument neither constructible nor a dangling case template requires(std::is_reference_v && !std::is_constructible_v && !detail::unexpect_dangles_v) - constexpr expected(unexpect_t, Args&&...) = delete; + constexpr expected(unexpect_t, Args&&...) = BEMAN_EXPECTED_DELETE_MSG( + "expected: no viable conversion from the given argument(s) to E&"); // In-place constructor for error with initializer_list template @@ -2571,11 +2599,13 @@ class expected { // Deleted for reference E with value G: would rebind E& to unexpected's temporary storage. template requires(std::is_reference_v && !std::is_reference_v) - constexpr expected& operator=(const unexpected&) = delete; + constexpr expected& operator=(const unexpected&) = BEMAN_EXPECTED_DELETE_MSG( + "expected: cannot assign from unexpected; the value would dangle — use unexpected"); template requires(std::is_reference_v && !std::is_reference_v) - constexpr expected& operator=(unexpected&&) = delete; + constexpr expected& operator=(unexpected&&) = BEMAN_EXPECTED_DELETE_MSG( + "expected: cannot assign from unexpected; the value would dangle — use unexpected"); // emplace — rebind the reference template diff --git a/include/beman/expected/unexpected.hpp b/include/beman/expected/unexpected.hpp index 9a269b4..ab95efe 100644 --- a/include/beman/expected/unexpected.hpp +++ b/include/beman/expected/unexpected.hpp @@ -10,6 +10,15 @@ #include #endif +// Deleted-function diagnostic messages (P2573, C++26 `= delete("reason")`). Falls back to a plain +// `= delete` pre-C++26 so this header keeps compiling at the project's configured floor; no +// behavioral difference either way, just a worse diagnostic on older compilers. +#if defined(__cpp_deleted_function) && __cpp_deleted_function >= 202403L + #define BEMAN_EXPECTED_DELETE_MSG(msg) delete (msg) +#else + #define BEMAN_EXPECTED_DELETE_MSG(msg) delete +#endif + namespace beman { namespace expected { @@ -132,14 +141,16 @@ class unexpected { // Deleted: binding would dangle (G materializes a temporary) template requires(detail::reference_constructs_from_temporary_v) - constexpr unexpected(G&&) = delete; + constexpr unexpected(G&&) = BEMAN_EXPECTED_DELETE_MSG( + "unexpected: argument would bind a temporary that dangles; pass an lvalue reference"); // Deleted catch-all: neither constructible nor a dangling case template requires(!std::is_same_v, unexpected> && !std::is_same_v, std::in_place_t> && !std::is_constructible_v && !detail::reference_constructs_from_temporary_v) - constexpr unexpected(G&&) = delete; + constexpr unexpected(G&&) = + BEMAN_EXPECTED_DELETE_MSG("unexpected: no viable conversion from the given argument to E&"); // Single-argument in_place_t overload — lets expected's uniform // construct_at(addressof(unex_), std::in_place, args...) pattern work whether E is a @@ -152,7 +163,8 @@ class unexpected { template requires(detail::reference_constructs_from_temporary_v) - constexpr unexpected(std::in_place_t, G&&) = delete; + constexpr unexpected(std::in_place_t, G&&) = BEMAN_EXPECTED_DELETE_MSG( + "unexpected: in_place argument would bind a temporary that dangles; pass an lvalue reference"); constexpr unexpected& operator=(const unexpected&) = default; constexpr unexpected& operator=(unexpected&&) = default; diff --git a/tests/beman/expected/expected_ref_e.test.cpp b/tests/beman/expected/expected_ref_e.test.cpp index dec4377..0af4751 100644 --- a/tests/beman/expected/expected_ref_e.test.cpp +++ b/tests/beman/expected/expected_ref_e.test.cpp @@ -13,6 +13,27 @@ using namespace beman::expected; +// ============================================================================= +// Finding 5: feature-test macro for the reference-E / reference-T extensions +// ============================================================================= + +#ifndef __cpp_lib_expected_ref + #error "__cpp_lib_expected_ref must be defined by " +#endif +static_assert(__cpp_lib_expected_ref > 0); + +// ============================================================================= +// Finding 4: guarded delete-with-message macro (falls back to plain `delete` +// pre-C++26; either way, the deleted overload stays deleted). +// ============================================================================= + +static_assert(!std::is_constructible_v, int&&>, + "unexpected dangling-temporary ctor must stay deleted regardless of " + "BEMAN_EXPECTED_DELETE_MSG's expansion"); +static_assert(!std::is_default_constructible_v>, + "expected must stay non-default-constructible regardless of " + "BEMAN_EXPECTED_DELETE_MSG's expansion"); + // ============================================================================= // Type-level static assertions // ============================================================================= From 177805b50a964a3807bbc9bd73371bbb68198bbc Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sat, 8 Aug 2026 23:24:30 -0400 Subject: [PATCH 7/7] style: apply clang-format Formatting only, no semantic change. Fixes the pre-commit/clang-format CI failure on this branch. --- include/beman/expected/expected.hpp | 28 ++++++++----------- .../beman/expected/expected_ref_both.test.cpp | 6 ++-- .../expected/expected_void_ref_e.test.cpp | 6 ++-- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/include/beman/expected/expected.hpp b/include/beman/expected/expected.hpp index b90309c..6119005 100644 --- a/include/beman/expected/expected.hpp +++ b/include/beman/expected/expected.hpp @@ -285,8 +285,8 @@ class expected { template requires(std::is_reference_v && !std::is_constructible_v && !detail::unexpect_dangles_v) - constexpr expected(unexpect_t, Args&&...) = BEMAN_EXPECTED_DELETE_MSG( - "expected: no viable conversion from the given argument(s) to E&"); + constexpr expected(unexpect_t, Args&&...) = + BEMAN_EXPECTED_DELETE_MSG("expected: no viable conversion from the given argument(s) to E&"); // In-place constructor for error with initializer_list template @@ -1457,8 +1457,7 @@ class expected { // implementations of reference_constructs_from_temporary_v resolve as a circular constraint. template requires(std::is_void_v && !std::is_reference_v && !std::is_same_v && - std::is_constructible_v && - !std::is_constructible_v, expected&> && + std::is_constructible_v && !std::is_constructible_v, expected&> && !std::is_constructible_v, expected &&> && !std::is_constructible_v, const expected&> && !std::is_constructible_v, const expected &&>) @@ -1466,8 +1465,7 @@ class expected { template requires(std::is_void_v && !std::is_reference_v && !std::is_same_v && - std::is_constructible_v && - !std::is_constructible_v, expected&> && + std::is_constructible_v && !std::is_constructible_v, expected&> && !std::is_constructible_v, expected &&> && !std::is_constructible_v, const expected&> && !std::is_constructible_v, const expected &&>) @@ -1525,8 +1523,8 @@ class expected { template requires(std::is_reference_v && !std::is_constructible_v && !detail::unexpect_dangles_v) - constexpr expected(unexpect_t, Args&&...) = BEMAN_EXPECTED_DELETE_MSG( - "expected: no viable conversion from the given argument(s) to E&"); + constexpr expected(unexpect_t, Args&&...) = + BEMAN_EXPECTED_DELETE_MSG("expected: no viable conversion from the given argument(s) to E&"); // In-place constructor for error with initializer_list template @@ -1670,7 +1668,7 @@ class expected { requires std::is_reference_v constexpr void value_or(U&&) const = BEMAN_EXPECTED_DELETE_MSG("expected: value_or is not defined for void value_type; there is no " - "value to fall back from — use has_value()/error()"); + "value to fall back from — use has_value()/error()"); // ------------------------------------------------------------------------- // [expected.void.monadic] Monadic operations @@ -1775,8 +1773,7 @@ constexpr expected::expected(expected&& rhs) noexcept(std::is_nothrow_m template template requires(std::is_void_v && !std::is_reference_v && !std::is_same_v && - std::is_constructible_v && - !std::is_constructible_v, expected&> && + std::is_constructible_v && !std::is_constructible_v, expected&> && !std::is_constructible_v, expected &&> && !std::is_constructible_v, const expected&> && !std::is_constructible_v, const expected &&>) @@ -1787,8 +1784,7 @@ constexpr expected::expected(const expected& rhs) : has_val_(rhs. template template - requires(std::is_void_v && !std::is_reference_v && !std::is_same_v && - std::is_constructible_v && + requires(std::is_void_v && !std::is_reference_v && !std::is_same_v && std::is_constructible_v && !std::is_constructible_v, expected&> && !std::is_constructible_v, expected &&> && !std::is_constructible_v, const expected&> && @@ -2413,7 +2409,7 @@ class expected { template constexpr expected(std::in_place_t, Args&&...) = BEMAN_EXPECTED_DELETE_MSG("expected: no in-place value constructor; T& cannot be constructed " - "in-place — pass a U convertible to T&"); + "in-place — pass a U convertible to T&"); // Value constructor — takes U that can bind to T& template @@ -2509,8 +2505,8 @@ class expected { template requires(std::is_reference_v && !std::is_constructible_v && !detail::unexpect_dangles_v) - constexpr expected(unexpect_t, Args&&...) = BEMAN_EXPECTED_DELETE_MSG( - "expected: no viable conversion from the given argument(s) to E&"); + constexpr expected(unexpect_t, Args&&...) = + BEMAN_EXPECTED_DELETE_MSG("expected: no viable conversion from the given argument(s) to E&"); // In-place constructor for error with initializer_list template diff --git a/tests/beman/expected/expected_ref_both.test.cpp b/tests/beman/expected/expected_ref_both.test.cpp index 7d66cb1..d228edf 100644 --- a/tests/beman/expected/expected_ref_both.test.cpp +++ b/tests/beman/expected/expected_ref_both.test.cpp @@ -202,8 +202,7 @@ TEST_CASE("expected: transition from value to error via copy assignment", CHECK(&a.error() == &err); } -TEST_CASE("expected: copy assignment rebinds error, does not assign through", - "[expected_ref_both]") { +TEST_CASE("expected: copy assignment rebinds error, does not assign through", "[expected_ref_both]") { int e1 = 1, e2 = 2; expected a(unexpect, e1); expected b(unexpect, e2); @@ -213,8 +212,7 @@ TEST_CASE("expected: copy assignment rebinds error, does not assign CHECK(e1 == 1); } -TEST_CASE("expected: move assignment rebinds error, does not assign through", - "[expected_ref_both]") { +TEST_CASE("expected: move assignment rebinds error, does not assign through", "[expected_ref_both]") { int e1 = 1, e2 = 2; expected a(unexpect, e1); expected b(unexpect, e2); diff --git a/tests/beman/expected/expected_void_ref_e.test.cpp b/tests/beman/expected/expected_void_ref_e.test.cpp index 91965ab..a9c6dac 100644 --- a/tests/beman/expected/expected_void_ref_e.test.cpp +++ b/tests/beman/expected/expected_void_ref_e.test.cpp @@ -112,8 +112,7 @@ TEST_CASE("expected: convert from expected", "[expected_void_ // Finding 2: the rvalue overload of the reference-E converting constructor was missing; // only the const& form existed. Verify the && form works and still binds the external // referent (never dangles — G is itself a reference to the caller's object). -TEST_CASE("expected: convert from expected&& binds external referent", - "[expected_void_ref_e]") { +TEST_CASE("expected: convert from expected&& binds external referent", "[expected_void_ref_e]") { int err = 9; expected src(unexpect, err); expected dst(std::move(src)); @@ -121,8 +120,7 @@ TEST_CASE("expected: convert from expected&& binds external r CHECK(&dst.error() == &err); } -TEST_CASE("expected: convert from expected (lvalue and rvalue)", - "[expected_void_ref_e]") { +TEST_CASE("expected: convert from expected (lvalue and rvalue)", "[expected_void_ref_e]") { int err = 11; expected src1(unexpect, err); expected dst1(src1);