Skip to content

Refactor fn::expected to decouple it from C++23#202

Draft
Bronek wants to merge 29 commits into
mainfrom
bronek/experimental_fn_base_refactor
Draft

Refactor fn::expected to decouple it from C++23#202
Bronek wants to merge 29 commits into
mainfrom
bronek/experimental_fn_base_refactor

Conversation

@Bronek

@Bronek Bronek commented May 22, 2026

Copy link
Copy Markdown
Member

No description provided.

@Bronek Bronek added the c++20 Support for C++20 label May 22, 2026
@codecov

codecov Bot commented May 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@Bronek Bronek force-pushed the bronek/experimental_fn_base_refactor branch from 7436c71 to 1f3c02e Compare May 23, 2026 16:01
@Bronek Bronek force-pushed the bronek/experimental_fn_base_refactor branch from 785baaf to 0eb827f Compare June 3, 2026 15:06
Bronek added 7 commits June 5, 2026 11:37
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
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
Bronek added 3 commits June 5, 2026 16:57
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
@sonarqubecloud

sonarqubecloud Bot commented Jun 5, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++20 Support for C++20

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant