diff --git a/include/fn/detail/functional.hpp b/include/fn/detail/functional.hpp index c27511a8..294917bd 100644 --- a/include/fn/detail/functional.hpp +++ b/include/fn/detail/functional.hpp @@ -19,7 +19,22 @@ namespace fn::detail { namespace _fold_detail { -template [[nodiscard]] constexpr auto _fold(auto &&l, auto &&r) +// The branch the fold takes is chosen by `if constexpr`, so a single expression cannot state its +// specification: the two untaken spellings would be ill-formed. Hence one specialization per branch. +template +struct _nothrow_fold : ::std::bool_constant{::std::declval(), ::std::declval()})> {}; +template + requires _some_pack +struct _nothrow_fold + : ::std::bool_constant().append(::std::in_place_type_t{}, ::std::declval()))> {}; +template + requires(not _some_pack) && _some_pack +struct _nothrow_fold : ::std::bool_constant{::std::declval()}.append( + ::std::in_place_type_t{}, ::std::declval()))> {}; + +template +[[nodiscard]] constexpr auto _fold(auto &&l, auto &&r) // + noexcept(_nothrow_fold::value) { if constexpr (_some_pack) { return FWD(l).append(::std::in_place_type_t{}, FWD(r)); @@ -32,35 +47,70 @@ template [[nodiscard]] constexpr auto _fold(auto &&l, a } } +// Named types, not lambdas: `fold` and everything above it specify themselves in terms of what +// dispatching through these promises, and a lambda can be named neither in a noexcept-specifier nor +// (before clang 17) in any unevaluated operand at all. +template struct _fold_rh final { + Rv rv; + + template + [[nodiscard]] constexpr auto operator()(::std::in_place_type_t, auto &&l) const + noexcept(noexcept(_fold(FWD(l), ::std::declval()))) + { + return _fold(FWD(l), static_cast(rv)); + } +}; + +template struct _fold_lh final { + Lv lv; + + template + [[nodiscard]] constexpr auto operator()(::std::in_place_type_t, auto &&r) const + noexcept(noexcept(_fold(::std::declval(), FWD(r)))) + { + return _fold(static_cast(lv), FWD(r)); + } +}; + +template struct _fold_rh_sum final { + Rv rv; + + template + [[nodiscard]] constexpr auto operator()(::std::in_place_type_t, auto &&l) const + noexcept(noexcept(::std::declval()._transform(_fold_lh{FWD(l)}))) + { + return static_cast(rv)._transform(_fold_lh{FWD(l)}); + } +}; + template requires _some_sum && _some_sum -[[nodiscard]] constexpr auto fold(auto &&lv, auto &&rv) +[[nodiscard]] constexpr auto fold(auto &&lv, auto &&rv) // + noexcept(noexcept(FWD(lv)._transform(_fold_rh_sum{FWD(rv)}))) { - return FWD(lv)._transform([&rv](::std::in_place_type_t, auto &&l) { - return FWD(rv)._transform( - [&l](::std::in_place_type_t, auto &&r) { return _fold(FWD(l), FWD(r)); }); - }); + return FWD(lv)._transform(_fold_rh_sum{FWD(rv)}); } template requires _some_sum && (not _some_sum) -[[nodiscard]] constexpr auto fold(auto &&lv, auto &&rv) +[[nodiscard]] constexpr auto fold(auto &&lv, auto &&rv) // + noexcept(noexcept(FWD(lv)._transform(_fold_rh{FWD(rv)}))) { - return FWD(lv)._transform( - [&rv](::std::in_place_type_t, auto &&l) { return _fold(FWD(l), FWD(rv)); }); + return FWD(lv)._transform(_fold_rh{FWD(rv)}); } template requires(not _some_sum) && _some_sum -[[nodiscard]] constexpr auto fold(auto &&lv, auto &&rv) +[[nodiscard]] constexpr auto fold(auto &&lv, auto &&rv) // + noexcept(noexcept(FWD(rv)._transform(_fold_lh{FWD(lv)}))) { - return FWD(rv)._transform( - [&lv](::std::in_place_type_t, auto &&r) { return _fold(FWD(lv), FWD(r)); }); + return FWD(rv)._transform(_fold_lh{FWD(lv)}); } template requires(not _some_sum) && (not _some_sum) -[[nodiscard]] constexpr auto fold(auto &&lv, auto &&rv) +[[nodiscard]] constexpr auto fold(auto &&lv, auto &&rv) // + noexcept(noexcept(_fold(FWD(lv), FWD(rv)))) { return _fold(FWD(lv), FWD(rv)); } diff --git a/include/fn/expected.hpp b/include/fn/expected.hpp index ce5feb41..48e6d7cd 100644 --- a/include/fn/expected.hpp +++ b/include/fn/expected.hpp @@ -57,16 +57,105 @@ template struct _expected_types<::fn::expected> { using error_type = G; }; +// A sum<> error is unconstructible, so an expected carrying one can never hold an error: every arm +// that lifts one is unreachable, and what cannot run cannot throw. Guarded on the SOURCE's error +// type - the value arms of the same expected still relocate, and still weigh. +template +constexpr inline bool _nothrow_error_arm = _nothrow_initializable; +template + requires ::std::is_same_v> +constexpr inline bool _nothrow_error_arm = true; + +// Carrying the callback's value across into a widened result. An expected has no value to +// carry, and `declval()` is not a thing to ask about. +template +constexpr inline bool _nothrow_carry_value + = _nothrow_initializable().value())>; +template + requires ::std::is_void_v::value_type> +constexpr inline bool _nothrow_carry_value = _nothrow_initializable; + +// `and_then` and `or_else` each have two arms - the callback's own expected is returned, or the two +// error (value) types are widened into a sum - and `if constexpr` picks between them. A +// noexcept-specifier is an ordinary constant expression and cannot pick: the untaken arm's spelling +// would have to be well-formed too. Hence a trait, whose constrained specializations mirror the +// body's arms. Both lead with `_is_some_expected`, so a callback returning something else leaves the +// unconstrained primary to answer, and the body's static_assert to diagnose - a specification must +// not pre-empt that with a hard error. +template struct _nothrow_and_then : ::std::false_type {}; + +template + requires _is_some_expected<::std::remove_cvref_t::type> &> + && ::std::is_same_v::type>>::error_type, + E> +struct _nothrow_and_then + : ::std::bool_constant< + _is_nothrow_invocable::value // the callback + && ::std::is_nothrow_constructible_v<::std::remove_cvref_t::type>, + ::fn::unexpect_t, ErrArg>> {}; // lifting self's error + +template + requires _is_some_expected<::std::remove_cvref_t::type> &> + && (not ::std::is_same_v::type>>::error_type, + E>) +struct _nothrow_and_then { + using type = ::std::remove_cvref_t::type>; + using new_type = ::fn::expected>; + + static constexpr bool value + = _is_nothrow_invocable::value // the callback + && _nothrow_carry_value // carrying its value across + && _nothrow_initializable().error())> // widening its error + && _nothrow_error_arm; // widening self's error +}; + +// or_else's arms, mirrored the same way. ValArg is the type of self's value as the body relocates it +// (spelled through apply_const_lvalue_t by the caller, since for a void T there is no value to name). +template struct _nothrow_or_else : ::std::false_type {}; + +template + requires _is_some_expected<::std::remove_cvref_t::type> &> + && ::std::is_same_v< + typename _expected_types<::std::remove_cvref_t::type>>::value_type, + T> +struct _nothrow_or_else + : ::std::bool_constant< + _is_nothrow_invocable::value // the callback + && (::std::is_void_v + || _nothrow_initializable<::std::remove_cvref_t::type>, + ::std::in_place_t, ValArg>)> {}; // carrying self's value + +template + requires _is_some_expected<::std::remove_cvref_t::type> &> + && (not ::std::is_same_v< + typename _expected_types<::std::remove_cvref_t::type>>::value_type, + T>) +struct _nothrow_or_else { + using type = ::std::remove_cvref_t::type>; + using new_type = ::fn::expected, typename type::error_type>; + + static constexpr bool value + = _is_nothrow_invocable::value // the callback + && _nothrow_initializable // widening self's value + && _nothrow_carry_value // widening its value + && _nothrow_initializable().error())>; // carrying its error +}; + // Storage layer for ::fn::expected. Inherits the standard-conformant base from // pfn, then hides the four monadic static helpers with sum-widening variants // that materialise their result via `expected_policy::template type`. // The transform/transform_error helpers hand pfn's _expected_from_invoke constructors a // zero-argument thunk, so the result's member is direct-non-list-initialized from fn's own // _invoke (or sum::transform) result: no extra move, and immovable result types work. -// The statics carry the same extension noexcept as pfn's, spelled with the std traits: for -// the sum/pack dispatch and sum-widening extensions the lead conjunct is false (the callable -// is not directly invocable, or the result is differently shaped), so those are -// conservatively noexcept(false). +// The statics carry the same extension noexcept as pfn's, computed through fn's own machinery: the +// callback of a sum/pack dispatch is invoked through `_invoke`, not called directly, so it is +// `_is_nothrow_invocable` - not the std trait, which is false for a callable that is not directly +// invocable on a sum or a pack - that answers for it, and the widening arms are weighed by the +// traits above. template struct _expected_base : ::pfn::detail::_expected_base { using _pfn_base = ::pfn::detail::_expected_base; using _pfn_base::_pfn_base; @@ -74,11 +163,8 @@ template struct _expected_base : ::pfn::detail::_expect // and_then, non-void value type template static constexpr auto _and_then(Self &&self, Fn &&fn) // - noexcept(::std::is_same_v::type>>::error_type, - E> - && ::std::is_nothrow_invocable_v - && ::std::is_nothrow_constructible_v) // extension + noexcept(::fn::detail::_nothrow_and_then::value) // extension requires(not ::std::is_void_v) && ::fn::detail::_is_invocable::value && ::std::is_constructible_v { @@ -113,12 +199,8 @@ template struct _expected_base : ::pfn::detail::_expect // and_then, void value type template - static constexpr auto _and_then(Self &&self, Fn &&fn) // - noexcept(::std::is_same_v::type>>::error_type, - E> - && ::std::is_nothrow_invocable_v - && ::std::is_nothrow_constructible_v) // extension + static constexpr auto _and_then(Self &&self, Fn &&fn) // + noexcept(::fn::detail::_nothrow_and_then::value) // extension requires(::std::is_void_v) && ::fn::detail::_is_invocable::value && ::std::is_constructible_v { @@ -157,13 +239,8 @@ template struct _expected_base : ::pfn::detail::_expect // an ill-formed _value call (constrained away for void) from being a hard error. template static constexpr auto _or_else(Self &&self, Fn &&fn) // - noexcept(::std::is_same_v::type>>::value_type, - T> - && ::std::is_nothrow_invocable_v - && (::std::is_void_v - || ::std::is_nothrow_constructible_v< - T, ::fn::apply_const_lvalue_t>)) // extension + noexcept(::fn::detail::_nothrow_or_else>::value) requires ::fn::detail::_is_invocable::value && (::std::is_void_v || ::std::is_constructible_v) { @@ -213,7 +290,7 @@ template struct _expected_base : ::pfn::detail::_expect // new value/error is direct-non-list-initialized from the thunk's result (guaranteed elision). template static constexpr auto _transform(Self &&self, Fn &&fn) // - noexcept(::std::is_nothrow_invocable_v + noexcept(::fn::detail::_is_nothrow_invocable::value && ::std::is_nothrow_constructible_v) // extension requires(not ::std::is_void_v) && (not some_sum) && ::fn::detail::_is_invocable_if, Fn, decltype(_pfn_base::_value(FWD(self)))>::value @@ -236,7 +313,9 @@ template struct _expected_base : ::pfn::detail::_expect // 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) + static constexpr auto _transform(Self &&self, Fn &&fn) // + noexcept(noexcept(_pfn_base::_value(FWD(self)).transform(FWD(fn))) + && ::std::is_nothrow_constructible_v) // extension requires some_sum && ::fn::detail::_typelist_invocable && ::std::is_constructible_v { @@ -256,7 +335,7 @@ template struct _expected_base : ::pfn::detail::_expect // transform, void value type template static constexpr auto _transform(Self &&self, Fn &&fn) // - noexcept(::std::is_nothrow_invocable_v + noexcept(::fn::detail::_is_nothrow_invocable::value && ::std::is_nothrow_constructible_v) // extension requires(::std::is_void_v) && ::fn::detail::_is_invocable::value && ::std::is_constructible_v @@ -278,7 +357,7 @@ template struct _expected_base : ::pfn::detail::_expect // apply_const_lvalue_t for the same reason as _or_else's above) template static constexpr auto _transform_error(Self &&self, Fn &&fn) // - noexcept(::std::is_nothrow_invocable_v + noexcept(::fn::detail::_is_nothrow_invocable::value && (::std::is_void_v || ::std::is_nothrow_constructible_v< T, ::fn::apply_const_lvalue_t>)) // extension @@ -302,7 +381,11 @@ template struct _expected_base : ::pfn::detail::_expect // 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) + static constexpr auto _transform_error(Self &&self, Fn &&fn) // + noexcept(noexcept(_pfn_base::_error(FWD(self)).transform(FWD(fn))) + && (::std::is_void_v + || ::std::is_nothrow_constructible_v< + T, ::fn::apply_const_lvalue_t>)) // extension requires some_sum && ::fn::detail::_typelist_invocable && (::std::is_void_v || ::std::is_constructible_v) { @@ -1051,13 +1134,59 @@ template class expected : private detail::_expected_ba return FWD(src).sum_error(); } +namespace detail { +template struct _expected_type { + template using type = ::fn::expected; +}; + +// `error()` throws when the expected holds a value, but every arm below is reached only once +// `has_value()` has answered - so these ask what constructing the result promises, with the accessor +// spelled as a type rather than as a call which would drag its own throw in. +template +constexpr inline bool _nothrow_join_expected + = _nothrow_initializable + && _nothrow_initializable().error())> + && _nothrow_initializable().error())>; + +// Lifting an operand's error into a widened error type, through the sum<> guard of _nothrow_error_arm +// (the joins below assert the same unreachability with pfn::unreachable). +template +constexpr inline bool _nothrow_error_lift + = _nothrow_error_arm::error_type, Err, decltype(::std::declval().error())>; + +template +constexpr inline bool _nothrow_join_widened = _nothrow_initializable + && (_nothrow_error_lift && _nothrow_error_lift) + && _nothrow_initializable; + +// A named type, not a lambda: `operator&` specifies itself in terms of what lifting the error +// promises, and a lambda can be named neither in a noexcept-specifier nor (before clang 17) in any +// unevaluated operand at all. +template struct _expected_efn final { + // Explicit return type: for a sum<> (never-erroring) operand the else branch is the only one + // instantiated, and without this it would deduce void - poisoning _join's return-type deduction. + [[nodiscard]] constexpr auto operator()(auto &&v) const noexcept(_nothrow_error_lift>) + -> ::fn::unexpected + { + if constexpr (not ::std::is_same_v::error_type, sum<>>) { + return ::fn::unexpected(FWD(v).error()); + } else { + ::pfn::unreachable(); // LCOV_EXCL_LINE + } + } +}; +} // namespace detail + // When any of the sides is expected, we do not produce expected, ...> // Instead just elide void and carry non-void (or elide both voids if that's what we get) template requires some_expected_void && (not some_expected_void) && ::std::is_same_v::error_type, typename ::std::remove_cvref_t::error_type> -[[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) noexcept +[[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // + noexcept(detail::_nothrow_join_expected< + expected::value_type, typename ::std::remove_cvref_t::error_type>, + Lh, Rh, decltype(FWD(rh).value())>) { using error_type = ::std::remove_cvref_t::error_type; using value_type = ::std::remove_cvref_t::value_type; @@ -1076,7 +1205,13 @@ template typename ::std::remove_cvref_t::error_type>) && (some_sum::error_type> || some_sum::error_type>) -[[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) noexcept +[[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // + noexcept(detail::_nothrow_join_widened< + expected::value_type, + sum_for::error_type, + typename ::std::remove_cvref_t::error_type>>, + sum_for::error_type, typename ::std::remove_cvref_t::error_type>, + Lh, Rh, decltype(FWD(rh).value())>) { using new_error_type = sum_for::error_type, typename ::std::remove_cvref_t::error_type>; @@ -1101,7 +1236,10 @@ template requires(not some_expected_void) && some_expected_void && ::std::is_same_v::error_type, typename ::std::remove_cvref_t::error_type> -[[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) noexcept +[[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // + noexcept(detail::_nothrow_join_expected< + expected::value_type, typename ::std::remove_cvref_t::error_type>, + Lh, Rh, decltype(FWD(lh).value())>) { using error_type = ::std::remove_cvref_t::error_type; using value_type = ::std::remove_cvref_t::value_type; @@ -1120,7 +1258,13 @@ template typename ::std::remove_cvref_t::error_type>) && (some_sum::error_type> || some_sum::error_type>) -[[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) noexcept +[[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // + noexcept(detail::_nothrow_join_widened< + expected::value_type, + sum_for::error_type, + typename ::std::remove_cvref_t::error_type>>, + sum_for::error_type, typename ::std::remove_cvref_t::error_type>, + Lh, Rh, decltype(FWD(lh).value())>) { using new_error_type = sum_for::error_type, typename ::std::remove_cvref_t::error_type>; @@ -1145,7 +1289,8 @@ template requires some_expected_void && some_expected_void && ::std::is_same_v::error_type, typename ::std::remove_cvref_t::error_type> -[[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) noexcept +[[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // + noexcept(detail::_nothrow_join_expected::error_type>, Lh, Rh>) { using error_type = ::std::remove_cvref_t::error_type; using type = expected; @@ -1163,7 +1308,12 @@ template typename ::std::remove_cvref_t::error_type>) && (some_sum::error_type> || some_sum::error_type>) -[[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) noexcept +[[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // + noexcept(detail::_nothrow_join_widened< + expected::error_type, + typename ::std::remove_cvref_t::error_type>>, + sum_for::error_type, typename ::std::remove_cvref_t::error_type>, + Lh, Rh>) { using new_error_type = sum_for::error_type, typename ::std::remove_cvref_t::error_type>; @@ -1185,21 +1335,18 @@ template // Overloads when both sides are non-void, producing either of // expected, ...> or expected, pack...>, ...> -namespace detail { -template struct _expected_type { - template using type = ::fn::expected; -}; -} // namespace detail - template requires(not some_expected_void) && (not some_expected_void) && ::std::is_same_v::error_type, typename ::std::remove_cvref_t::error_type> -[[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) noexcept +[[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // + noexcept(noexcept(::fn::detail::_join< + detail::template _expected_type::error_type>::template type>( + FWD(lh), FWD(rh), detail::_expected_efn::error_type>{}))) { using error_type = ::std::remove_cvref_t::error_type; - constexpr auto efn = [](auto &&v) { return ::fn::unexpected(FWD(v).error()); }; - return ::fn::detail::_join::template type>(FWD(lh), FWD(rh), efn); + return ::fn::detail::_join::template type>( + FWD(lh), FWD(rh), detail::_expected_efn{}); } template @@ -1208,20 +1355,19 @@ template typename ::std::remove_cvref_t::error_type>) && (some_sum::error_type> || some_sum::error_type>) -[[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) noexcept +[[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // + noexcept(noexcept( + ::fn::detail::_join< + detail::template _expected_type::error_type, + typename ::std::remove_cvref_t::error_type>>::template type>( + FWD(lh), FWD(rh), + detail::_expected_efn::error_type, + typename ::std::remove_cvref_t::error_type>>{}))) { using new_error_type = sum_for::error_type, typename ::std::remove_cvref_t::error_type>; - // Explicit return type: for a sum<> (never-erroring) operand the else branch is the only one - // instantiated, and without this it would deduce void — poisoning _join's return-type deduction. - constexpr auto efn = [](auto &&v) -> ::fn::unexpected { - if constexpr (not ::std::is_same_v::error_type, sum<>>) { - return ::fn::unexpected(FWD(v).error()); - } else { - ::pfn::unreachable(); // LCOV_EXCL_LINE - } - }; - return ::fn::detail::_join::template type>(FWD(lh), FWD(rh), efn); + return ::fn::detail::_join::template type>( + FWD(lh), FWD(rh), detail::_expected_efn{}); } } // namespace fn diff --git a/include/fn/optional.hpp b/include/fn/optional.hpp index c9980f91..d4168621 100644 --- a/include/fn/optional.hpp +++ b/include/fn/optional.hpp @@ -127,23 +127,52 @@ struct optional_policy { template static constexpr bool is_specialization = _is_some_optional; }; +// `or_else` has two arms - the callback's own optional is returned, or its value type and T are +// widened into a sum - and `if constexpr` picks between them. A noexcept-specifier is an ordinary +// constant expression, so it cannot pick: the untaken arm's spelling would have to be well-formed +// too, and it is not. Hence a trait, whose constrained specializations mirror the body's arms. The +// unconstrained primary answers for a callback returning something that is not an optional at all - +// the body's static_assert is the diagnostic there, and it must not be pre-empted by a hard error +// in the specification. +template struct _nothrow_optional_or_else : ::std::false_type {}; + +template + requires ::std::is_same_v<::std::remove_cvref_t::type>, ::fn::optional> +struct _nothrow_optional_or_else + : ::std::bool_constant<_is_nothrow_invocable::value + && ::std::is_nothrow_constructible_v<::fn::optional, ::std::in_place_t, ValArg>> {}; + +template + requires _is_some_optional<::std::remove_cvref_t::type> &> + && (not ::std::is_same_v<::std::remove_cvref_t::type>, ::fn::optional>) +struct _nothrow_optional_or_else { + using type = ::std::remove_cvref_t::type>; + using new_type = ::fn::optional>; + + static constexpr bool value + = _is_nothrow_invocable::value // the callback + && _nothrow_initializable // self's value + && _nothrow_initializable().value())>; // its value +}; + // Storage layer for ::fn::optional. Inherits the standard-conformant base from // pfn, then hides the three monadic static helpers with sum-aware variants that // materialise their result via `optional_policy::template type`. // The transform helpers hand pfn's _optional_from_invoke constructor a zero-argument // thunk, so the result's contained value is direct-non-list-initialized from fn's own // _invoke (or sum::transform) result: no extra move, and immovable result types work. -// The statics carry the same extension noexcept as pfn's, spelled with the std traits: for -// the sum/pack dispatch extensions std::is_nothrow_invocable_v is false (the callable is not -// directly invocable on a sum or pack), so those are conservatively noexcept(false). +// The statics carry the same extension noexcept as pfn's, computed through fn's own machinery: +// the callback of a sum/pack dispatch is invoked through `_invoke`, not called directly, so it is +// `_is_nothrow_invocable` - not the std trait, which is false for a callable that is not directly +// invocable on a sum or a pack - that answers for it. template struct _optional_base : ::pfn::detail::_optional_base { using _pfn_base = ::pfn::detail::_optional_base; using _pfn_base::_pfn_base; // and_then template - static constexpr auto _and_then(Self &&self, Fn &&fn) // - noexcept(::std::is_nothrow_invocable_v) // extension + static constexpr auto _and_then(Self &&self, Fn &&fn) // + noexcept(::fn::detail::_is_nothrow_invocable::value) // extension requires ::fn::detail::_is_invocable::value { using type = ::std::remove_cvref_t::type>; @@ -169,11 +198,8 @@ template struct _optional_base : ::pfn::detail::_optional_base - static constexpr auto _or_else(Self &&self, Fn &&fn) // - noexcept( - ::std::is_same_v<::std::remove_cvref_t::type>, ::fn::optional> - && ::std::is_nothrow_invocable_v - && ::std::is_nothrow_constructible_v<::fn::optional, Self>) // extension + static constexpr auto _or_else(Self &&self, Fn &&fn) // + noexcept(::fn::detail::_nothrow_optional_or_else::value) // extension requires ::fn::detail::_is_invocable::value && ::std::is_constructible_v { using type = ::std::remove_cvref_t::type>; @@ -215,8 +241,8 @@ template struct _optional_base : ::pfn::detail::_optional_base - static constexpr auto _transform(Self &&self, Fn &&fn) // - noexcept(::std::is_nothrow_invocable_v) // extension + static constexpr auto _transform(Self &&self, Fn &&fn) // + noexcept(::fn::detail::_is_nothrow_invocable::value) // extension requires(not some_sum) && ::fn::detail::_is_invocable_if, Fn, decltype(*FWD(self))>::value { using new_value_type = ::std::remove_cv_t::type>; @@ -234,7 +260,8 @@ template struct _optional_base : ::pfn::detail::_optional_base - static constexpr auto _transform(Self &&self, Fn &&fn) + static constexpr auto _transform(Self &&self, Fn &&fn) // + noexcept(noexcept((*FWD(self)).transform(FWD(fn)))) // extension requires some_sum && ::fn::detail::_typelist_invocable { using new_value_type = decltype((*FWD(self)).transform(FWD(fn))); @@ -891,10 +918,19 @@ constexpr optional make_optional(::std::initializer_list il, Args &&...arg return FWD(src).sum_value(); } -template [[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) noexcept +namespace detail { +// A named type, not a lambda: `operator&`'s specification has to name it, and a lambda cannot be +// spelled in an unevaluated operand before C++20's P0315, which our floor compilers predate. +struct _optional_efn final { + [[nodiscard]] constexpr auto operator()(auto const &) const noexcept -> ::std::nullopt_t { return ::std::nullopt; } +}; +} // namespace detail + +template +[[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // + noexcept(noexcept(::fn::detail::_join(FWD(lh), FWD(rh), detail::_optional_efn{}))) { - constexpr auto efn = [](auto const &) { return ::std::nullopt; }; - return ::fn::detail::_join(FWD(lh), FWD(rh), efn); + return ::fn::detail::_join(FWD(lh), FWD(rh), detail::_optional_efn{}); } } // namespace fn diff --git a/include/fn/pack.hpp b/include/fn/pack.hpp index 9deb48c8..4e10930c 100644 --- a/include/fn/pack.hpp +++ b/include/fn/pack.hpp @@ -357,16 +357,36 @@ template namespace detail { +// `value()` throws when the monad holds no value, but the join only reaches it once `has_value()` +// has answered - so the specification below asks what folding and lifting the value promise, with +// the accessor spelled as a type rather than as a call which would drag its own throw in. +template using _value_of_t = decltype(::std::declval().value()); + +template +using _joined_t = decltype(::fn::detail::_fold_detail::fold::value_type, + typename ::std::remove_cvref_t::value_type>( + ::std::declval<_value_of_t>(), ::std::declval<_value_of_t>())); + +// `_join` invokes `efn` as an lvalue - a named parameter - so the `Efn &` questions ask about the +// call the body performs; the reference collapses to it whatever category the callable arrived in. +template