Target Audience: High-context Large Language Models acting as rigorous C++ code reviewers.
Scope: The entire include/beman/expected/ header set — one 3,413-line header (expected.hpp) plus two small companions (unexpected.hpp, bad_expected_access.hpp). The suite has 19 positive test files with 688 Catch2 TEST_CASEs and 59 negative compile tests.
You are reviewing a proposed C++29 reference implementation of std::expected that extends the C++26 standard with reference specializations. The implementation targets standardization via the Beman Project. Your job is not to praise working code — it is to find defects, inconsistencies, specification violations, and latent footguns. "Looks good" is not an acceptable conclusion unless you can demonstrate why every clause was checked.
| Source | Location | Use |
|---|---|---|
| C++26 standard wording | docs/standard/[expected].html |
Normative text for primary and void specializations |
| Standard (org-mode) | docs/standard/expected.org |
Same content, machine-parseable |
| Standard (plain text) | docs/standard/expected.txt |
Same content, grep-friendly |
| Conformance audit | docs/conformance-audit.md |
Every clause checked with PASS/EXT/FIXED status |
| P2988R12 (optional<T&>) | wg21.link/P2988R12 | Rebind semantics design source for reference specializations |
| beman/optional reference impl | ~/src/steve-downey/optional/main/ |
Pattern source for T& storage and dangling prevention |
This is critical. The reference specializations (expected<T&, E>, expected<T, E&>, expected<T&, E&>, expected<void, E&>) have no published standard wording. They are an extension proposed for C++29. The only design authority is:
- Analogy to P2988's
optional<T&>semantics (rebind, shallow const, dangling prevention) - The implementation's own
docs/plan/step7-*.mdthroughstep10-*.mddesign docs - Consistency with the primary template's established patterns
This means: for the primary template and void specialization, you can check clause-by-clause against [expected.object.*] and [expected.void.*]. For reference specializations, you must instead check internal consistency and design-principle adherence. Don't fabricate standard references that don't exist.
Before reading line-by-line, establish the shape:
- Count class-template forms. There should be exactly 3: primary
<T,E>, partial specialization<void,E>, and partial specialization<T&,E>. AllowingEto be an lvalue reference yields the six value/error configurations without three additional class definitions. - Identify the storage model for each form. Every error arm is
unexpected<E>; it owns an object for objectEand stores a pointer for referenceE. TheT&form stores its value as a pointer. Verify no union directly contains a reference. - Map the helper utilities.
reinit_expected(lines 65-84) handles destroy-and-reconstruct for value-type transitions.reference_constructs_from_temporary_v(lines 88-100) is the dangling-prevention concept. Verify these are used correctly and only where applicable.
For expected<T, E> and expected<void, E>, audit against the standard:
The standard distinguishes:
- Constraints (
requiresclause on declaration): SFINAE-friendly, checked during overload resolution - Mandates (
static_assertinside body): hard error on instantiation, not SFINAE-friendly
Check for these specific misclassifications:
- Is any Mandate implemented as a
requiresclause? (This would silently hide the error instead of diagnosing it) - Is any Constraint implemented as a
static_assert? (This would cause hard errors when SFINAE should apply) - The conformance audit at
docs/conformance-audit.mdclaims all clauses are correct — verify independently by sampling 3-5 constructors against[expected.object.cons]
Every converting constructor uses explicit(bool-expr). The standard specifies exactly which is_convertible_v checks determine explicitness. Verify:
- The logical structure matches (conjunction of negated
is_convertible_vchecks) - The value categories in the
is_convertible_varguments match what the standard says (const G&vsGvsG&&)
The implementation adds conditional noexcept on some constructors where the standard is silent. docs/conformance-audit.md marks these as "EXT" (conforming extension). Verify:
- These are genuinely conforming (strengthening, not weakening)
- They don't accidentally constrain the overload set via SFINAE on
noexcept
Since there's no standard wording, check:
Each reference specialization should offer the same user-facing operations as the primary, adapted for reference semantics. Missing operations are bugs unless explicitly justified. Check:
| Operation | <T,E> |
<T&,E> |
<T,E&> |
<T&,E&> |
<void,E&> |
|---|---|---|---|---|---|
| Default ctor | yes | deleted | yes | deleted | yes |
| Copy/move ctor | yes | yes | yes | yes | yes |
| Converting ctor | yes | yes (from U&) | yes | yes (from U&,G&) | yes |
| unexpected<G> ctor | yes | yes | from <G&> only |
from <G&> only |
from <G&> only |
| unexpect_t ctor | yes | yes | yes | yes | yes |
| in_place_t ctor | yes | deleted | yes | deleted | N/A (void) |
| Value assignment | yes | rebind | yes | yes | N/A |
| unexpected assignment | yes | yes | from <G&> only |
from <G&> only |
from <G&> only |
| emplace | yes | rebind | yes | rebind | emplace() (void) |
| operator* | T& | T& | T& | T& | void |
| operator-> | T* | T* | T* | T* | N/A |
| value() | throws | throws | throws | throws | throws |
| value_or | yes | yes | yes | yes | deleted |
| error_or | yes | yes | yes | yes | yes |
| Monadic (4 ops) | yes | yes | yes | yes | yes |
| swap | yes | yes | yes | yes | yes |
| equality | yes | yes | yes | yes | yes |
Verify each cell. Pay special attention to the deleted entries — each should have a = delete("message") with a clear diagnostic and a corresponding negative compile test.
For the from <G&> only cells: construction and rebinding assignment from unexpected<G> are permitted for reference E only when G is itself a reference (unexpected<E&>, which holds a pointer to an external object). The value-typed form stays deleted because it would dangle. Verify construction and assignment traits for the accepted reference case, rejected value case, and rejected const-dropping case.
For every constructor or assignment that accepts a forwarding reference where the destination is T& or E&:
- Verify
!detail::reference_constructs_from_temporary_v<T&, U>appears in therequiresclause - Verify a deleted overload exists for the case where temporaries would bind
- Verify the deleted overload's message is tested by a
_fail.cppnegative test
Known subtlety: For expected<int&, int> constructing from 42, the constraint rejects via "no matching function" (SFINAE), not via the deleted overload. The deleted overload fires only when reference_constructs_from_temporary_v is true (e.g., const int& from int). Both paths prevent the dangerous construction, but the diagnostic differs. This is correct behavior.
expected<T&, E>: union ofT*andunexpected<E>plusbool has_val_expected<T, E&>: union ofTand pointer-holdingunexpected<E&>plusbool has_val_expected<T&, E&>: union ofT*and pointer-holdingunexpected<E&>plusbool has_val_expected<void, E&>: pointer-holdingunexpected<E&>plusbool has_val_
Verify:
- No union contains a reference or pointer where the active member tracking could be wrong
- Assignment to reference specializations reassigns pointers (rebind), never destroys/reconstructs
reinit_expectedis NOT called for pointer-only transitions
For each monadic operation across all specializations, verify:
- The callable receives the correct value category of the stored value
- For
T&specializations: callable always receivesT&(dereferenced pointer), regardless of the expected object's own value category - For
E&specializations: error forwarding passesE&(dereferenced pointer), neverE&& - Return type wrapping preserves the reference nature where appropriate
Every function in this implementation should be constexpr. Verify:
- Union member activation via placement-new or
std::construct_atis constexpr-valid - Pointer dereference in reference specializations is constexpr-valid
- No
reinterpret_castorvoid*arithmetic that would fail constant evaluation
The primary and void forms must be trivially copyable/movable/destructible when their stored alternatives permit it. Pointer-only T&/E& configurations should be trivial, while expected<T&, E> still follows object-E properties. Verify:
= defaultpaths exist for trivial cases- The conditional dispatch between trivial and non-trivial paths is correct
- Reference specializations don't accidentally have non-trivial destructors
Under #if defined(BEMAN_EXPECTED_HARDENED):
operator*andoperator->trap when!has_value()error()traps whenhas_value()- Verify these guards exist in ALL specializations, not just the primary
Every converting constructor/assignment from expected<U, G> or unexpected<G> must build the target value/error by moving the wrapper, then accessing — *std::move(rhs), std::move(rhs).error(), std::move(e).error() — never by accessing, then moving — std::move(*rhs), std::move(rhs.error()), std::move(e.error()).
The deref-then-move form is a defect for reference specializations: the accessor is shallow and returns a reference to an external object, and the std::move then steals from it — the boost::optional<T&> move-steal bug. Both forms compile and are indistinguishable by eye, so verify by behavior, not inspection:
std::string s="bar"; expected<std::string&,int> r{s}; expected<std::string,int> o{std::move(r)};must leaves == "bar"(copied, not stolen). Same for the error axis (expected<int,std::string&>→expected<int,std::string>) and forunexpected<E&>sources, on both construction and assignment.- A genuine value source (
expected<std::string,int>/unexpected<std::string>rvalue) must still move (source left empty).
Mechanical audit: grep the header for std::move(* and std::move(<ident>.value()/error()) — every hit is a candidate defect; the safe forms wrap the whole object (*std::move(x), std::move(x).accessor()). This rests on the invariant that rvalue accessors are shallow for reference specializations and deep for value ones (Phase 2 accessor checks).
These are areas where the implementation may have latent bugs. Investigate each:
-
expected<bool, E>value constructor ambiguity. The implementation could not add a= delete("message")overload for the case whereexpected<bool, E>is constructed from anotherexpectedspecialization (because the deleted overload conflicts with the converting constructor). The regex forexpected_bool_value_ctor_from_expected_failis still the generic "no matching function". Is this a real problem, or is the constraint sufficient? -
Move-only error types in reference specializations. The monadic constraint tests (
expected_monadic_constraints.test.cpp) only cover the primary and void specializations. Forexpected<T&, E>with a move-onlyE: areand_thenandtransformcorrectly constrained on their lvalue overloads (which needis_constructible_v<E, E&>)? This is untested. -
expected<T&, E&>equality operators. When comparingexpected<T&, E&> == expected<U&, G&>, does the comparison dereference both sides correctly? Is there a test for comparing two reference-expected objects that point to the same underlying object? -
value_orfor reference specializations. Forexpected<T&, E>, what doesvalue_or(default_ref)return? AT&? AT? The return type semantics differ fundamentally from the primary template. Verify the return type is documented and tested. -
Swap for
expected<T&, E&>. Swapping two pointer-only expected objects should be trivial (swap pointers and bool). Verify no unnecessary destroy/reconstruct logic is invoked.
Structure your review as:
## [Specialization or Section Name]
### Finding N: [severity: defect|concern|nit|question]
**Location:** file:line
**Standard reference:** [clause] (or "no normative source — checking internal consistency")
**Issue:** [description]
**Evidence:** [code quote or reasoning]
**Suggested fix:** [if applicable]
Do not pad with praise. Every section must contain at least one finding or an explicit "verified correct, N clauses checked" with clause references.