Refactor fn::expected to decouple it from C++23#202
Draft
Bronek wants to merge 29 commits into
Draft
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
7436c71 to
1f3c02e
Compare
785baaf to
0eb827f
Compare
clang miscompiles `return type();` (and `return type{};`, `return type{in_place};`)
in fn::detail::_storage::_or_else when T is void and self is in value state,
for three of the four Self ref-qualifier instantiations (&, const &, const &&).
The returned expected ends up observed as has_value() == false even though
the path through _or_else verifiably constructs a value-state result.
Naming a local `result` variable forces NRVO/move semantics and dodges the
buggy mandatory-elision lowering. Safe specifically here because the void
case constructs an empty expected<void, Err> with no user value forwarding,
so we don't need mandatory copy elision to support non-copyable user types.
Sibling branches (non-void value preservation, sum_for path) keep their
prvalue returns and remain unaffected.
Assisted-by: Claude:claude-opus-4-7
Assisted-by: Claude:claude-opus-4-8
Assisted-by: Claude:claude-opus-4-8
Assisted-by: Claude:claude-opus-4-8
The value-forwarding ctor expected(U&&) must reject a same-type U, or it out-competes copy/move for same-type construction and silently hijacks them (for a non-const lvalue, U&& deduces an exact-match expected&, beating the copy ctor's const expected&). The guard is _can_convert<U>'s `not is_same_v<Policy::type<T,E>, remove_cvref_t<U>>`. For a normal T it is backstopped by is_constructible_v<T,U>; for a greedy T (constructible from expected<T,E>) it is the only thing standing, and it leans on the Policy::type<T,E> indirection resolving correctly -- a piece that could rot with no diagnostic, especially for fn::expected's own policy. greedy_t is constructible from anything yet non-copyable/non-movable, so an expected<greedy_t,Error> constructible from itself could only be that hijack; the static_asserts pin it at compile time. Verified live by neutralising the exclusion (the asserts fire, including for fn::expected via the nested polyfill suite) and holds against std::expected (validation build). SonarQube flags this ctor for the same reason but cannot see through the _can_convert trait -- a false positive; this test is the durable guard rather than a duplicated constraint. Assisted-by: Claude:claude-opus-4-8
The trivial defaulted move constructors are constrained to is_trivially_move_constructible T/E, which is always nothrow, so they are now unconditionally noexcept; their non-trivial sibling overloads keep the conditional spec. Together they reproduce the standard's single conditional move ctor ([expected.object.cons]/15, [expected.void.cons]/7) for the trivial case, so triviality and the observable exception spec are unchanged. Move assignment and swap keep their conditional noexcept(...) and are marked // NOSONAR cpp:S5018: forcing them unconditionally noexcept would be a bug -- they handle possibly-throwing T/E and must let the exception propagate, as mandated by [expected.object.assign]/9, [expected.object.swap]/4, [expected.void.assign]/8, [expected.void.swap]/4. All six specs verified against the working draft. Assisted-by: Claude:claude-opus-4-8
…ding) The unexpected<G> const& constructor uses std::forward<const G&>(g.error()) -- a no-op identity cast that cpp:S6031 flags, but it is exactly the standard's Effects wording: [expected.object.cons]/29 specifies "... with std::forward<GF>(e.error())", with GF = const G& for this overload. pfn mirrors the standard verbatim, so this is a false positive -- suppress rather than rewrite. (Reverts the earlier std::move/g.error() rewrite, which deviated from the spec.) Assisted-by: Claude:claude-opus-4-8
Equivalent to the named-local form -- materialise the value-state result, then move-construct the return, avoiding the direct-prvalue guaranteed-elision path that clang 15-18 miscompile -- but one line, with the required move semantics visible at the call site. No -Wpessimizing-move because `type` is dependent. Verified on clang-18: compiles -Wall -Wextra -Werror clean and the _or_else suite passes at -O1/-O2/-O3 (the prvalue form fails it). gcc and clang-19+ are unaffected -- the #else branch is unchanged. Assisted-by: Claude:claude-opus-4-8
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



No description provided.