diff --git a/include/fn/expected.hpp b/include/fn/expected.hpp index 25dbeff8..dbe265d6 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; @@ -282,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; @@ -296,10 +299,12 @@ 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 + && (::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/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..bf11fa86 100644 --- a/tests/fn/expected.cpp +++ b/tests/fn/expected.cpp @@ -8,6 +8,7 @@ #include +#include #include #include @@ -2576,6 +2577,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 +2762,50 @@ 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}); + } + + 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]") 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}); + } }