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/include/beman/expected/expected.hpp b/include/beman/expected/expected.hpp index 6ad625e..6119005 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 @@ -309,8 +320,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 +339,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 && @@ -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 @@ -715,8 +728,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 +756,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 && @@ -1443,16 +1456,16 @@ 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 && - !std::is_constructible_v, expected&> && + 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&> && !std::is_constructible_v, const 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 && - !std::is_constructible_v, expected&> && + 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&> && !std::is_constructible_v, const expected &&>) @@ -1483,11 +1496,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; @@ -1500,25 +1515,37 @@ 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 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 // ------------------------------------------------------------------------- @@ -1543,7 +1570,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 +1583,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)); @@ -1583,11 +1610,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; @@ -1637,7 +1666,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 @@ -1741,8 +1772,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 && - !std::is_constructible_v, expected&> && + 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&> && !std::is_constructible_v, const expected &&>) @@ -1753,7 +1784,7 @@ 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 +1840,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 // ============================================================================= @@ -1835,7 +1876,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 +1898,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)) { @@ -2347,7 +2388,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. @@ -2366,7 +2407,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 @@ -2383,7 +2426,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 @@ -2438,11 +2482,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 @@ -2452,13 +2498,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 @@ -2489,7 +2537,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 +2550,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)); @@ -2547,11 +2595,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 @@ -2831,7 +2881,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 +2904,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/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/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)} 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 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, diff --git a/tests/beman/expected/expected_ref_both.test.cpp b/tests/beman/expected/expected_ref_both.test.cpp index 3f58da9..d228edf 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,26 @@ 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..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 // ============================================================================= @@ -37,6 +58,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 +164,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..a9c6dac 100644 --- a/tests/beman/expected/expected_void_ref_e.test.cpp +++ b/tests/beman/expected/expected_void_ref_e.test.cpp @@ -30,6 +30,31 @@ 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>); + +// 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 // --------------------------------------------------------------------------- @@ -84,6 +109,30 @@ 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 // --------------------------------------------------------------------------- @@ -122,6 +171,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 // ---------------------------------------------------------------------------