From d6092368d9c5c006c29fff6742ebb6b78e4158d3 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sat, 11 Jul 2026 19:17:07 +0100 Subject: [PATCH 1/2] Constrain the callback of the sum-case `transform` and `transform_error` The sum-delegating statics constrained only the grade, leaving callback validity to be discovered inside the deduced-return body. Two consequences, both outside the immediate context: a callback no alternative could take was a hard error where the non-sum path cleanly drops the candidate, and overload resolution was poisoned - the losing `const &` candidate formed its signature too, so a visitor serving only the category the call selects made the call ill-formed. Constraining the callback in the immediate context, as `sum::transform` itself does, means a partial visitor is now enough. The existing tests spell all four value categories in every visitor because they had to; the new blocks assert they no longer do. Closes #277 Assisted-by: Claude:claude-opus-4-8[1m] --- include/fn/expected.hpp | 11 +++++++---- include/fn/optional.hpp | 8 ++++++-- tests/fn/expected.cpp | 40 ++++++++++++++++++++++++++++++++++++++++ tests/fn/optional.cpp | 21 +++++++++++++++++++++ 4 files changed, 74 insertions(+), 6 deletions(-) diff --git a/include/fn/expected.hpp b/include/fn/expected.hpp index 25dbeff8..b9dc2854 100644 --- a/include/fn/expected.hpp +++ b/include/fn/expected.hpp @@ -233,10 +233,12 @@ template struct _expected_base : ::pfn::detail::_expect return type(::fn::unexpect, _pfn_base::_error(FWD(self))); } - // transform, value type is a sum (delegates to sum::transform) + // transform, value type is a sum (delegates to sum::transform). The callback is constrained here, + // in the immediate context, for the reason given on optional's sum-case _transform. template static constexpr auto _transform(Self &&self, Fn &&fn) - requires some_sum && ::std::is_constructible_v + requires some_sum && ::fn::detail::_typelist_invocable + && ::std::is_constructible_v { using new_value_type = decltype(_pfn_base::_value(FWD(self)).transform(FWD(fn))); using type = ::fn::expected; @@ -296,10 +298,11 @@ template struct _expected_base : ::pfn::detail::_expect }); } - // transform_error, error type is a sum (delegates to sum::transform) + // transform_error, error type is a sum (delegates to sum::transform). The callback is constrained + // here, in the immediate context, for the reason given on optional's sum-case _transform. template static constexpr auto _transform_error(Self &&self, Fn &&fn) - requires some_sum + requires some_sum && ::fn::detail::_typelist_invocable { using new_error_type = decltype(_pfn_base::_error(FWD(self)).transform(FWD(fn))); using type = ::fn::expected; diff --git a/include/fn/optional.hpp b/include/fn/optional.hpp index 10832f7d..f0937a97 100644 --- a/include/fn/optional.hpp +++ b/include/fn/optional.hpp @@ -228,10 +228,14 @@ template struct _optional_base : ::pfn::detail::_optional_base static constexpr auto _transform(Self &&self, Fn &&fn) - requires some_sum + requires some_sum && ::fn::detail::_typelist_invocable { using new_value_type = decltype((*FWD(self)).transform(FWD(fn))); using type = ::fn::optional; diff --git a/tests/fn/expected.cpp b/tests/fn/expected.cpp index c92f0a60..e18d6abd 100644 --- a/tests/fn/expected.cpp +++ b/tests/fn/expected.cpp @@ -2576,6 +2576,26 @@ TEST_CASE("expected sum support transform", "[expected][sum][transform]") // TODO Switch bool to std::monostate or similar user-defined type static_assert(a.transform(fn).value().has_value()); } + + WHEN("constraints") + { + using T = fn::expected, Error>; + constexpr auto can_transform_lval = [](auto &&f) { return requires { std::declval().transform(f); }; }; + constexpr auto can_transform_clval = [](auto &&f) { return requires { std::declval().transform(f); }; }; + + // a callback no alternative can take drops the candidate, rather than failing inside the body + static_assert(not can_transform_lval([](double) -> bool { throw 0; })); + static_assert(not can_transform_lval([](int &) -> bool { throw 0; })); // string_view is unhandled + + // a visitor need only serve the value category the call actually selects + constexpr auto lval_only + = fn::overload{[](int &i) -> bool { return i == 12; }, [](std::string_view &) -> bool { throw 0; }}; + static_assert(can_transform_lval(lval_only)); + static_assert(not can_transform_clval(lval_only)); + + T s{fn::sum{12}}; + CHECK(s.transform(lval_only).value() == fn::sum{true}); + } } TEST_CASE("expected sum support transform_error", "[expected][sum][transform_error]") @@ -2741,6 +2761,26 @@ TEST_CASE("expected sum support transform_error", "[expected][sum][transform_err static_assert(a.transform_error(fn).error() == fn::sum{42}); } } + + WHEN("constraints") + { + using T = fn::expected>; + constexpr auto can_lval = [](auto &&f) { return requires { std::declval().transform_error(f); }; }; + constexpr auto can_clval = [](auto &&f) { return requires { std::declval().transform_error(f); }; }; + + // a callback no alternative can take drops the candidate, rather than failing inside the body + static_assert(not can_lval([](double) -> bool { throw 0; })); + static_assert(not can_lval([](int &) -> bool { throw 0; })); // string_view is unhandled + + // a visitor need only serve the value category the call actually selects + constexpr auto lval_only + = fn::overload{[](int &i) -> bool { return i == 12; }, [](std::string_view &) -> bool { throw 0; }}; + static_assert(can_lval(lval_only)); + static_assert(not can_clval(lval_only)); + + T s{::fn::unexpect, fn::sum{12}}; + CHECK(s.transform_error(lval_only).error() == fn::sum{true}); + } } TEST_CASE("expected pack support or_else", "[expected][or_else][pack]") diff --git a/tests/fn/optional.cpp b/tests/fn/optional.cpp index 8f3f8089..b060c27e 100644 --- a/tests/fn/optional.cpp +++ b/tests/fn/optional.cpp @@ -619,4 +619,25 @@ TEST_CASE("optional transform sum", "[optional][sum][transform]") static_assert(std::is_same_v>>); static_assert(a.transform(fn).value() == fn::sum{true}); } + + WHEN("constraints") + { + using T = fn::optional>; + constexpr auto can_transform_lval = [](auto &&f) { return requires { std::declval().transform(f); }; }; + constexpr auto can_transform_clval = [](auto &&f) { return requires { std::declval().transform(f); }; }; + + // a callback no alternative can take drops the candidate, rather than failing inside the body + static_assert(not can_transform_lval([](std::string_view) -> bool { throw 0; })); + static_assert(not can_transform_lval([](int &) -> bool { throw 0; })); // Xint is unhandled + + // a visitor need only serve the value category the call actually selects: the losing const& + // candidate is dropped by its own constraint rather than forming its signature and poisoning + // the call. The four-category visitors above are a spelling choice, not a requirement. + constexpr auto lval_only = fn::overload{[](int &i) -> bool { return i == 12; }, [](Xint &) -> bool { throw 0; }}; + static_assert(can_transform_lval(lval_only)); + static_assert(not can_transform_clval(lval_only)); + + T s{12}; + CHECK(s.transform(lval_only).value() == fn::sum{true}); + } } From 23c55fa60b2c73e66d9be1447ca63bf539a6a3bd Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sat, 11 Jul 2026 19:20:38 +0100 Subject: [PATCH 2/2] Require of `transform_error` that the untouched value can be relocated The body puts the untouched value into the result, but only the noexcept spec weighed that copy - the requires-clause did not, alone among the monadic statics (`_or_else`, `_and_then` and `_transform` all carry their untouched-side conjunct). The copy was therefore checked while instantiating the deduced-return body, and since `const &` binds rvalues, the losing candidate hard-errored for a move-only value type: `fn::expected` could not call `transform_error` from any value category, where `pfn::expected` supports the rvalue call and cleanly drops the lvalue one. A pfn -> fn switch turned valid code ill-formed. Closes #278 Assisted-by: Claude:claude-opus-4-8[1m] --- include/fn/expected.hpp | 2 ++ tests/fn/expected.cpp | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/include/fn/expected.hpp b/include/fn/expected.hpp index b9dc2854..dbe265d6 100644 --- a/include/fn/expected.hpp +++ b/include/fn/expected.hpp @@ -284,6 +284,7 @@ template struct _expected_base : ::pfn::detail::_expect T, ::fn::apply_const_lvalue_t>)) // extension requires(not some_sum) && ::fn::detail::_is_invocable_if, Fn, decltype(_pfn_base::_error(FWD(self)))>::value + && (::std::is_void_v || ::std::is_constructible_v) { using new_error_type = typename ::fn::detail::_invoke_result::type; using type = ::fn::expected; @@ -303,6 +304,7 @@ template struct _expected_base : ::pfn::detail::_expect template static constexpr auto _transform_error(Self &&self, Fn &&fn) requires some_sum && ::fn::detail::_typelist_invocable + && (::std::is_void_v || ::std::is_constructible_v) { using new_error_type = decltype(_pfn_base::_error(FWD(self)).transform(FWD(fn))); using type = ::fn::expected; diff --git a/tests/fn/expected.cpp b/tests/fn/expected.cpp index e18d6abd..bf11fa86 100644 --- a/tests/fn/expected.cpp +++ b/tests/fn/expected.cpp @@ -8,6 +8,7 @@ #include +#include #include #include @@ -2781,6 +2782,30 @@ TEST_CASE("expected sum support transform_error", "[expected][sum][transform_err T s{::fn::unexpect, fn::sum{12}}; CHECK(s.transform_error(lval_only).error() == fn::sum{true}); } + + WHEN("untouched value is relocated, so a move-only value type drops the copying overloads") + { + // the conjunct or_else carries and pfn's transform_error requires: the untouched value goes + // into the result, so only the overloads whose self can be moved from survive + constexpr auto generic = [](auto &&) -> bool { throw 0; }; + + using M = fn::expected, fn::sum>; // sum-case overload + constexpr auto can_M_lval = [](auto &&f) { return requires { std::declval().transform_error(f); }; }; + constexpr auto can_M_rval = [](auto &&f) { return requires { std::declval().transform_error(f); }; }; + static_assert(not can_M_lval(generic)); // would copy the value + static_assert(can_M_rval(generic)); // moves it + + using N = fn::expected, Error>; // non-sum overload + constexpr auto can_N_lval = [](auto &&f) { return requires { std::declval().transform_error(f); }; }; + constexpr auto can_N_rval = [](auto &&f) { return requires { std::declval().transform_error(f); }; }; + static_assert(not can_N_lval(generic)); + static_assert(can_N_rval(generic)); + + N n{std::make_unique(7)}; + auto r = std::move(n).transform_error([](Error const &) -> bool { throw 0; }); + static_assert(std::is_same_v, bool>>); + CHECK(*r.value() == 7); + } } TEST_CASE("expected pack support or_else", "[expected][or_else][pack]")