Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
5c417b9
Docs & metadata: fix stale badges, broken links, citation, and doc drift
steve-downey Aug 6, 2026
43a2773
Design paper: fix first/second ordinal; describe guarded delete messages
steve-downey Aug 6, 2026
5ef26d4
Fix: expected<...,E&> copy/move assignment incorrectly disabled for c…
steve-downey Aug 7, 2026
2f5a05f
Fix: expected<void,E&> unsafe/inconsistent converting constructor fro…
steve-downey Aug 7, 2026
b482340
Standardese: make reference-E wording coherent; fix typo and undefine…
steve-downey Aug 7, 2026
8b82470
Add guarded delete-with-message macro and __cpp_lib_expected_ref (no …
steve-downey Aug 7, 2026
4cca335
Merge remote-tracking branch 'origin/main' into fix/ref-e-correctness
steve-downey Aug 9, 2026
d353148
Merge remote-tracking branch 'origin/main' into fix/design-paper-corr…
steve-downey Aug 9, 2026
82b24b1
Merge remote-tracking branch 'origin/main' into fix/docs-and-metadata
steve-downey Aug 9, 2026
33b36d2
Merge remote-tracking branch 'origin/main' into fix/paper-wording-coh…
steve-downey Aug 9, 2026
177805b
style: apply clang-format
steve-downey Aug 9, 2026
dfb8647
Merge pull request #6 from steve-downey/fix/design-paper-corrections
steve-downey Aug 9, 2026
d5da274
Merge pull request #7 from steve-downey/fix/docs-and-metadata
steve-downey Aug 9, 2026
744ac15
Merge pull request #8 from steve-downey/fix/paper-wording-coherence
steve-downey Aug 9, 2026
118135f
Merge pull request #9 from steve-downey/fix/ref-e-correctness
steve-downey Aug 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-->

<!-- markdownlint-disable-next-line line-length -->
[![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)

Expand Down
2 changes: 1 addition & 1 deletion docs/conformance-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | |

Expand Down
2 changes: 1 addition & 1 deletion docs/human-design-review-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ This matches `T* const` (const pointer to non-const T), `std::reference_wrapper`

### What was done

For `expected<T, E&>`, `expected<T&, E&>`, and `expected<void, E&>`, construction from `unexpected<G>` is allowed **only when `G` is itself a reference** (i.e. from `unexpected<E&>`), and `= delete`d when `G` is a value type. Assignment from `unexpected<G>` is still not offered for reference `E` (construction-only, for now).
For `expected<T, E&>`, `expected<T&, E&>`, and `expected<void, E&>`, construction from `unexpected<G>` is allowed **only when `G` is itself a reference** (i.e. from `unexpected<E&>`), and `= delete`d when `G` is a value type. Rebinding assignment from `unexpected<E&>` is now supported alongside construction, following the same reference-`G`-only rule.

```cpp
int err = 42;
Expand Down
7 changes: 3 additions & 4 deletions docs/std-parity.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# std::expected Parity

Before adding the reference specializations (`expected<T&, E>` 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<T&, E>` 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

Expand Down
146 changes: 98 additions & 48 deletions include/beman/expected/expected.hpp

Large diffs are not rendered by default.

18 changes: 15 additions & 3 deletions include/beman/expected/unexpected.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@
#include <utility>
#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 {

Expand Down Expand Up @@ -132,14 +141,16 @@ class unexpected<E&> {
// Deleted: binding would dangle (G materializes a temporary)
template <class G>
requires(detail::reference_constructs_from_temporary_v<E&, G>)
constexpr unexpected(G&&) = delete;
constexpr unexpected(G&&) = BEMAN_EXPECTED_DELETE_MSG(
"unexpected<E&>: argument would bind a temporary that dangles; pass an lvalue reference");

// Deleted catch-all: neither constructible nor a dangling case
template <class G>
requires(!std::is_same_v<std::remove_cvref_t<G>, unexpected> &&
!std::is_same_v<std::remove_cvref_t<G>, std::in_place_t> && !std::is_constructible_v<E&, G &&> &&
!detail::reference_constructs_from_temporary_v<E&, G>)
constexpr unexpected(G&&) = delete;
constexpr unexpected(G&&) =
BEMAN_EXPECTED_DELETE_MSG("unexpected<E&>: 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
Expand All @@ -152,7 +163,8 @@ class unexpected<E&> {

template <class G>
requires(detail::reference_constructs_from_temporary_v<E&, G>)
constexpr unexpected(std::in_place_t, G&&) = delete;
constexpr unexpected(std::in_place_t, G&&) = BEMAN_EXPECTED_DELETE_MSG(
"unexpected<E&>: in_place argument would bind a temporary that dangles; pass an lvalue reference");

constexpr unexpected& operator=(const unexpected&) = default;
constexpr unexpected& operator=(unexpected&&) = default;
Expand Down
23 changes: 14 additions & 9 deletions papers/D4280R0.tex
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand Down Expand Up @@ -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<T\&,E>: 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<T\&,E>: 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<E>} is exposition-only; the observable
behavior is keyed on \tcode{E} (D12)}
Expand Down
68 changes: 54 additions & 14 deletions papers/expected-new.tex
Original file line number Diff line number Diff line change
Expand Up @@ -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}%
Expand Down Expand Up @@ -712,7 +712,7 @@
\item
\tcode{is_copy_constructible_v<T>} is \tcode{true} and
\item
\tcode{is_copy_constructible_v<E>} is \tcode{true}.
\tcode{is_copy_constructible_v<E>} is \tcode{true}\added{ or \tcode{is_reference_v<E>} is \tcode{true}}.
\end{itemize}

\pnum
Expand All @@ -737,7 +737,7 @@
\item
\tcode{is_move_constructible_v<T>} is \tcode{true} and
\item
\tcode{is_move_constructible_v<E>} is \tcode{true}.
\tcode{is_move_constructible_v<E>} is \tcode{true}\added{ or \tcode{is_reference_v<E>} is \tcode{true}}.
\end{itemize}

\pnum
Expand Down Expand Up @@ -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}.
Expand All @@ -1089,9 +1097,9 @@
\item
\tcode{is_copy_constructible_v<T>} is \tcode{true} and
\item
\tcode{is_copy_assignable_v<E>} is \tcode{true} and
\tcode{is_copy_assignable_v<E>} is \tcode{true}\added{ or \tcode{is_reference_v<E>} is \tcode{true}} and
\item
\tcode{is_copy_constructible_v<E>} is \tcode{true} and
\tcode{is_copy_constructible_v<E>} is \tcode{true}\added{ or \tcode{is_reference_v<E>} is \tcode{true}} and
\item
\tcode{is_nothrow_move_constructible_v<T> || is_nothrow_move_constructible_v<E>}
is \tcode{true}.
Expand Down Expand Up @@ -1123,9 +1131,9 @@
\item
\tcode{is_move_assignable_v<T>} is \tcode{true} and
\item
\tcode{is_move_constructible_v<E>} is \tcode{true} and
\tcode{is_move_constructible_v<E>} is \tcode{true}\added{ or \tcode{is_reference_v<E>} is \tcode{true}} and
\item
\tcode{is_move_assignable_v<E>} is \tcode{true} and
\tcode{is_move_assignable_v<E>} is \tcode{true}\added{ or \tcode{is_reference_v<E>} is \tcode{true}} and
\item
\tcode{is_nothrow_move_constructible_v<T> || is_nothrow_move_constructible_v<E>}
is \tcode{true}.
Expand Down Expand Up @@ -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}.
Expand Down Expand Up @@ -2101,7 +2117,7 @@
\pnum
\remarks
This constructor is defined as deleted
unless \tcode{is_copy_constructible_v<E>} is \tcode{true}.
unless \tcode{is_copy_constructible_v<E>} is \tcode{true}\added{ or \tcode{is_reference_v<E>} is \tcode{true}}.

\pnum
This constructor is trivial
Expand All @@ -2116,7 +2132,7 @@
\begin{itemdescr}
\pnum
\constraints
\tcode{is_move_constructible_v<E>} is \tcode{true}.
\tcode{is_move_constructible_v<E>} is \tcode{true}\added{ or \tcode{is_reference_v<E>} is \tcode{true}}.

\pnum
\effects
Expand Down Expand Up @@ -2321,15 +2337,27 @@
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}.

\pnum
\remarks
This operator is defined as deleted unless
\tcode{is_copy_assignable_v<E>} is \tcode{true} and
\tcode{is_copy_constructible_v<E>} is \tcode{true}.
\begin{itemize}
\item
\tcode{is_copy_assignable_v<E>} is \tcode{true}\added{ or \tcode{is_reference_v<E>} is \tcode{true}} and
\item
\tcode{is_copy_constructible_v<E>} is \tcode{true}\added{ or \tcode{is_reference_v<E>} is \tcode{true}}.
\end{itemize}

\pnum
This operator is trivial if
Expand All @@ -2347,8 +2375,12 @@
\begin{itemdescr}
\pnum
\constraints
\tcode{is_move_constructible_v<E>} is \tcode{true} and
\tcode{is_move_assignable_v<E>} is \tcode{true}.
\begin{itemize}
\item
\tcode{is_move_constructible_v<E>} is \tcode{true}\added{ or \tcode{is_reference_v<E>} is \tcode{true}} and
\item
\tcode{is_move_assignable_v<E>} is \tcode{true}\added{ or \tcode{is_reference_v<E>} is \tcode{true}}.
\end{itemize}

\pnum
\effects
Expand All @@ -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}.
Expand Down Expand Up @@ -2631,7 +2671,7 @@

\pnum
\constraints
\tcode{is_constructible_v<E, decltype(error())>>} is \tcode{true}.
\tcode{is_constructible_v<E, decltype(error())>} is \tcode{true}.

\pnum
\mandates
Expand Down
4 changes: 2 additions & 2 deletions papers/mybiblio.bib
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
26 changes: 26 additions & 0 deletions tests/beman/expected/expected_ref_both.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ static_assert(std::is_trivially_copy_assignable_v<expected<int&, int&>>);
static_assert(std::is_trivially_move_assignable_v<expected<int&, int&>>);
static_assert(std::is_trivially_destructible_v<expected<int&, int&>>);

// Finding 1: copy/move assignment must be available for const-reference E, where E
// itself is not assignable (is_copy_assignable_v<const int&> is false) but the
// stored unexpected<E&> rebinds via pointer assignment.
static_assert(std::is_copy_assignable_v<expected<int&, const int&>>);
static_assert(std::is_move_assignable_v<expected<int&, const int&>>);

// operator-> returns T* (shallow const)
static_assert(std::is_same_v<decltype(std::declval<expected<int&, int&>>().operator->()), int*>);
static_assert(std::is_same_v<decltype(std::declval<const expected<int&, int&>>().operator->()), int*>);
Expand Down Expand Up @@ -196,6 +202,26 @@ TEST_CASE("expected<T&,E&>: transition from value to error via copy assignment",
CHECK(&a.error() == &err);
}

TEST_CASE("expected<T&,const E&>: copy assignment rebinds error, does not assign through", "[expected_ref_both]") {
int e1 = 1, e2 = 2;
expected<int&, const int&> a(unexpect, e1);
expected<int&, const int&> b(unexpect, e2);
a = b;
REQUIRE(!a.has_value());
CHECK(&a.error() == &e2);
CHECK(e1 == 1);
}

TEST_CASE("expected<T&,const E&>: move assignment rebinds error, does not assign through", "[expected_ref_both]") {
int e1 = 1, e2 = 2;
expected<int&, const int&> a(unexpect, e1);
expected<int&, const int&> 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<G>) exists for expected<T&, E&> — it would bind E&
// to temporary storage creating a dangling reference.
Expand Down
Loading
Loading