Conversation
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.
There was a problem hiding this comment.
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.
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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>
Resolves #26.
Implements a C++11-compatible polyfill of
std::expected<T, E>(P0323).Surface
expected<T, E>and theexpected<void, E>specializationunexpected<E>(converting /in_place/ init-list ctors,error()×4,swap,operator==, deduction guide)bad_expected_access<void>:std::exception;bad_expected_access<E>(storesE,error()×4)unexpect_t/unexpectand_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 freeswapoperator==always;operator!=below C++20 (C++20+ rewrites from==)Standard reference
T/Eare rejected viastatic_assert, matchingstd::expected.Design notes
detail::cond_trivial_smf(asoptional/variantdo) with both arms in the triviality pack, plus anexpected_assign_guardbase for the extra[expected.object.assign]nothrow-move deletion thatcond_trivial_smfcan't express.valuelessstate keepscond_trivial_smf's default-then-fill contract satisfied while the type is never observably valueless.reinit-expectedthree-rung strategy (nothrow-construct / temp-then-move / backup-then-restore) for the strong exception guarantee; the third rung'stry/catchis gated to C++20constexpr.YK_POLYFILL_CXX*_CONSTEXPR/YK_POLYFILL_NODISCARD; no<compare>(there is nooperator<=>).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
expectedcases pass on every combination. MSVC not exercised locally — relies on CI.