Skip to content

Add expected - #43

Open
yaito3014 wants to merge 6 commits into
mainfrom
expected
Open

Add expected#43
yaito3014 wants to merge 6 commits into
mainfrom
expected

Conversation

@yaito3014

Copy link
Copy Markdown
Owner

Resolves #26.

Implements a C++11-compatible polyfill of std::expected<T, E> (P0323).

Surface

  • expected<T, E> and the expected<void, E> specialization
  • unexpected<E> (converting / in_place / init-list ctors, error()×4, swap, operator==, deduction guide)
  • bad_expected_access<void> : std::exception; bad_expected_access<E> (stores E, error()×4)
  • unexpect_t / unexpect
  • Monadic and_then / or_else / transform / transform_error (4 ref-qual overloads each; void-value variants call the callable with no argument)
  • value_or (non-void) / error_or; member and free swap
  • Comparisons: operator== always; operator!= below C++20 (C++20+ rewrites from ==)

Standard reference T/E are rejected via static_assert, matching std::expected.

Design notes

  • Storage layers over detail::cond_trivial_smf (as optional/variant do) with both arms in the triviality pack, plus an expected_assign_guard base for the extra [expected.object.assign] nothrow-move deletion that cond_trivial_smf can't express.
  • A union with a transient valueless state keeps cond_trivial_smf's default-then-fill contract satisfied while the type is never observably valueless.
  • Arm-switching assignment uses the standard's reinit-expected three-rung strategy (nothrow-construct / temp-then-move / backup-then-restore) for the strong exception guarantee; the third rung's try/catch is gated to C++20 constexpr.
  • Feature-gated via YK_POLYFILL_CXX*_CONSTEXPR / YK_POLYFILL_NODISCARD; no <compare> (there is no operator<=>).

Tests

test/cxx{11,14,17,20}/expected.cpp (cxx11 is the comprehensive bulk; higher standards add constexpr depth, CTAD, and !=-rewrite checks), including dedicated cases for the reinit strong guarantee and the assign-SMF deletion.

Verification

Built and ran locally with g++-14 and clang++-21 across C++11/14/17/20 (real Catch2 v2 for C++11, v3 for C++14+); full C++11 suite and the expected cases pass on every combination. MSVC not exercised locally — relies on CI.

Implement a C++11-compatible polyfill of std::expected<T, E> (P0323):
expected<T, E> and the expected<void, E> specialization, unexpected<E>,
bad_expected_access<void>/<E>, unexpect_t/unexpect, the monadic
and_then/or_else/transform/transform_error, value_or/error_or, member and
free swap, and comparisons (operator== always; operator!= below C++20).

Storage reuses detail::cond_trivial_smf over a union with a transient
valueless state, so the type is never observably valueless while switching
arms; the reinit-expected three-rung strategy provides the strong exception
guarantee. Tests cover C++11/14/17/20.

Resolves #26.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a polyfill implementation of C++23's std::expected and std::unexpected under the yk::polyfill namespace, along with comprehensive test suites for C++11, C++14, C++17, and C++20. The review feedback identifies conformance issues in the emplace overloads of expected, which are incorrectly constrained on std::is_nothrow_constructible instead of std::is_constructible. Additionally, the feedback highlights a mismatch in constexpr qualifiers for internal const&& qualified helpers (base_get_value and base_get_error) that could cause compilation failures in C++11.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread include/yk/polyfill/expected.hpp
Comment thread include/yk/polyfill/expected.hpp
Comment thread include/yk/polyfill/expected.hpp Outdated
Comment thread include/yk/polyfill/expected.hpp Outdated
yaito3014 and others added 5 commits July 17, 2026 01:27
Two conformance divergences from std::expected, both found by comparing
traits against the real std::expected on g++ 15.2 and clang 21.

[expected.object.cons] applies the converts-from-any-cvref check only
"if T is not cv bool"; expected_can_convert applied it unconditionally,
so is_constructible_v<expected<bool, int>, expected<int, int>> was false
where std reports true. The unexpected-side check keeps no carve-out, as
the standard specifies.

The member swaps enforced their requirements with a static_assert rather
than SFINAE, and the free swap omitted both the is_swappable clauses and
the (is_nothrow_move_constructible<T> || is_nothrow_move_constructible<E>)
clause of [expected.object.swap]. is_swappable_v<expected<T, E>> therefore
reported true for types where neither arm is nothrow-move-constructible,
and an actual swap was a hard error instead of a removed overload. Both
member swaps now use the enable_if pattern unexpected::swap already uses,
each carrying its own clause set ([expected.void.swap] takes E alone), and
the free swap mirrors them dispatching on is_void<T>.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The swap regression test asserted is_swappable<expected<ThrowMove,
ThrowMove>> is false, which fails on all four MSVC C++14 jobs while every
GCC/Clang job and every MSVC C++17+ job passes.

std::swap only gained its is_move_constructible && is_move_assignable
constraint in C++17. MSVC's STL gates it on _HAS_CXX17; libstdc++ and
libc++ apply it in all modes as an extension. is_swappable resolves
swap(t&, t&) with using std::swap, so on MSVC at C++14 an unconstrained
std::swap stays viable and the trait reports true no matter how the free
swap for expected is constrained. That the repo's existing (green) test
already proves is_move_assignable<expected<ThrowMove, ThrowMove>> is false
there leaves no other explanation.

This is a limitation of is_swappable below C++17, not of expected, so probe
polyfill's own free swap through a qualified pf::swap in the C++11 test,
which observes the constraint regardless of std::swap, and keep the
end-to-end is_swappable check in the C++17 test where std::swap is
constrained. Both still fail against the pre-fix header.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lue() mandates

Four divergences from the current draft, each confirmed by differential
probes against real std::expected on g++ 15 and clang 21.

The standard's void partial specialization is constrained by is_void_v<T>
and so covers const/volatile-qualified void; the polyfill only specialized
plain void, sending expected<const void, E> into the primary template's
static_assert. The constrained specialization is not expressible before
C++20, so the void class body moves to bits/expected_void.ipp and is
stamped out once per cv-qualification (the pattern function_ref.ipp
already uses). value_type keeps its cv-qualification, the void
comparisons accept any cv-void pair, expected_void_can_convert carries
the source U, and the void or_else mandates the exact same value_type
per [expected.void.monadic].

[expected.object.monadic] constrains and_then/transform on
is_constructible_v<E, decltype((error()))> and or_else/transform_error on
is_constructible_v<T, decltype((val))>, per ref-qualifier; the polyfill
implemented only the Mandates, leaving overloads visible to SFINAE
detection where std removes them. All 24 constrained overloads now carry
the matching enable_if; the void or_else/transform_error correctly get
none.

expected(U&&) and operator=(U&&) default U to remove_cv_t<T>, not T
([expected.object.cons] p23, [expected.object.assign] p8): with
cv-qualified T and a braced-init argument the old default built a const
temporary and copied where std moves.

value() rvalue overloads mandate is_copy_constructible_v<E> in addition
to move-constructibility ([expected.object.obs] p15); the polyfill
accepted move-only E where std is ill-formed. The void specialization's
value() overloads get their [expected.void.obs] mandates likewise.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
[expected.object.obs] specifies value()& throws
bad_expected_access(as_const(error())), copying the error from E const&.
The polyfill passed E&, so an E whose E(E&) overload is deleted selected
the deleted constructor and failed to compile where std::expected works.
Cast to E const& at the throw site; the const& overload already passes
E const& and the rvalue overloads already match the std::move forms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add expected

1 participant