pfn's _transform_error requires is_constructible_v<T, decltype(_value(FWD(self)))> (include/pfn/detail/expected_base.hpp:904). fn drops that conjunct:
- non-sum overload (
include/fn/expected.hpp:283-284) — no value-copy conjunct, while the body copies the untouched value at :290;
- sum-case overload (:302) — constrained on
some_sum<E> alone.
The copy is therefore checked only during deduced-return body instantiation, outside the immediate context — and since const& binds rvalues, even an rvalue call forms the losing const& candidate, whose body hard-errors on the copy. Verified on g++ and clang++:
fn::expected<move_only, E> cannot call transform_error from any value category;
pfn::expected<move_only, E> handles the same code correctly — rvalue calls work, lvalue calls SFINAE-drop.
A pfn→fn switch thus turns valid code ill-formed, violating the superset principle. Internal asymmetry confirms an oversight rather than a design: fn's own _or_else (expected.hpp:168), _and_then (:83) and _transform (:220) all carry their untouched-side copy conjunct; _transform_error alone omits it.
Fix: add (std::is_void_v<T> || std::is_constructible_v<T, decltype(_pfn_base::_value(FWD(self)))>) to both transform_error overloads' requires-clauses (the _or_else spelling at :168). The sum case additionally needs the callback constraint — #277.
Tests (branch bronek/unit_tests_cleanup, commit b88b64e): the or_else-vs-transform_error contrast is asserted with a move-only value type on the or_else side (clean SFINAE drops); the transform_error side carries GAP comments only, since any probe hard-errors until fixed.
pfn's
_transform_errorrequiresis_constructible_v<T, decltype(_value(FWD(self)))>(include/pfn/detail/expected_base.hpp:904). fn drops that conjunct:include/fn/expected.hpp:283-284) — no value-copy conjunct, while the body copies the untouched value at :290;some_sum<E>alone.The copy is therefore checked only during deduced-return body instantiation, outside the immediate context — and since
const&binds rvalues, even an rvalue call forms the losingconst&candidate, whose body hard-errors on the copy. Verified on g++ and clang++:fn::expected<move_only, E>cannot calltransform_errorfrom any value category;pfn::expected<move_only, E>handles the same code correctly — rvalue calls work, lvalue calls SFINAE-drop.A pfn→fn switch thus turns valid code ill-formed, violating the superset principle. Internal asymmetry confirms an oversight rather than a design: fn's own
_or_else(expected.hpp:168),_and_then(:83) and_transform(:220) all carry their untouched-side copy conjunct;_transform_erroralone omits it.Fix: add
(std::is_void_v<T> || std::is_constructible_v<T, decltype(_pfn_base::_value(FWD(self)))>)to bothtransform_erroroverloads' requires-clauses (the_or_elsespelling at :168). The sum case additionally needs the callback constraint — #277.Tests (branch
bronek/unit_tests_cleanup, commit b88b64e): the or_else-vs-transform_error contrast is asserted with a move-only value type on the or_else side (clean SFINAE drops); the transform_error side carries GAP comments only, since any probe hard-errors until fixed.