diff --git a/include/fn/expected.hpp b/include/fn/expected.hpp index dbe265d6..ce5feb41 100644 --- a/include/fn/expected.hpp +++ b/include/fn/expected.hpp @@ -632,8 +632,12 @@ template class expected : private detail::_expected_b return _base::_transform_error(::std::move(*this), FWD(f)); } - // Convert to graded monad - auto sum_error() const & -> expected> + // Convert to graded monad. A lifting overload wraps one side in a sum and relocates the other + // untouched, so it weighs both; the ones whose side already is a sum only return *this. + constexpr auto sum_error() const & noexcept(::std::is_nothrow_constructible_v + && ::std::is_nothrow_constructible_v, error_type const &> + && ::std::is_nothrow_move_constructible_v>) // extension + -> expected> requires(not some_sum) { using type = expected>; @@ -642,7 +646,10 @@ template class expected : private detail::_expected_b else return type{::fn::unexpect, sum(this->error())}; } - auto sum_error() && -> expected> + constexpr auto sum_error() && noexcept(::std::is_nothrow_constructible_v + && ::std::is_nothrow_constructible_v, error_type> + && ::std::is_nothrow_move_constructible_v>) // extension + -> expected> requires(not some_sum) { using type = expected>; @@ -651,28 +658,32 @@ template class expected : private detail::_expected_b else return type{::fn::unexpect, sum(::std::move(*this).error())}; } - auto sum_error() & -> decltype(auto) + constexpr auto sum_error() & noexcept -> decltype(auto) requires(some_sum) { return *this; } - auto sum_error() const & -> decltype(auto) + constexpr auto sum_error() const & noexcept -> decltype(auto) requires(some_sum) { return *this; } - auto sum_error() && -> decltype(auto) + constexpr auto sum_error() && noexcept -> decltype(auto) requires(some_sum) { return ::std::move(*this); } - auto sum_error() const && -> decltype(auto) + constexpr auto sum_error() const && noexcept -> decltype(auto) requires(some_sum) { return ::std::move(*this); } - auto sum_value() const & -> expected, error_type> + constexpr auto + sum_value() const & noexcept(::std::is_nothrow_constructible_v, value_type const &> + && ::std::is_nothrow_move_constructible_v> + && ::std::is_nothrow_constructible_v) // extension + -> expected, error_type> requires(not some_sum) { using type = expected, error_type>; @@ -681,7 +692,10 @@ template class expected : private detail::_expected_b else return type{::fn::unexpect, this->error()}; } - auto sum_value() && -> expected, error_type> + constexpr auto sum_value() && noexcept(::std::is_nothrow_constructible_v, value_type> + && ::std::is_nothrow_move_constructible_v> + && ::std::is_nothrow_constructible_v) // extension + -> expected, error_type> requires(not some_sum) { using type = expected, error_type>; @@ -690,22 +704,22 @@ template class expected : private detail::_expected_b else return type{::fn::unexpect, ::std::move(*this).error()}; } - auto sum_value() & -> decltype(auto) + constexpr auto sum_value() & noexcept -> decltype(auto) requires(some_sum) { return *this; } - auto sum_value() const & -> decltype(auto) + constexpr auto sum_value() const & noexcept -> decltype(auto) requires(some_sum) { return *this; } - auto sum_value() && -> decltype(auto) + constexpr auto sum_value() && noexcept -> decltype(auto) requires(some_sum) { return ::std::move(*this); } - auto sum_value() const && -> decltype(auto) + constexpr auto sum_value() const && noexcept -> decltype(auto) requires(some_sum) { return ::std::move(*this); @@ -971,8 +985,10 @@ template class expected : private detail::_expected_ba return _base::_transform_error(::std::move(*this), FWD(f)); } - // Convert to graded monad - auto sum_error() const & -> expected> + // Convert to graded monad. There is no value to relocate here, so only the error's lift weighs. + constexpr auto sum_error() const & noexcept(::std::is_nothrow_constructible_v, error_type const &> + && ::std::is_nothrow_move_constructible_v>) // extension + -> expected> requires(not some_sum) { using type = expected>; @@ -981,7 +997,9 @@ template class expected : private detail::_expected_ba else return type{::fn::unexpect, sum(this->error())}; } - auto sum_error() && -> expected> + constexpr auto sum_error() && noexcept(::std::is_nothrow_constructible_v, error_type> + && ::std::is_nothrow_move_constructible_v>) // extension + -> expected> requires(not some_sum) { using type = expected>; @@ -990,22 +1008,22 @@ template class expected : private detail::_expected_ba else return type{::fn::unexpect, sum(::std::move(*this).error())}; } - auto sum_error() & -> decltype(auto) + constexpr auto sum_error() & noexcept -> decltype(auto) requires(some_sum) { return *this; } - auto sum_error() const & -> decltype(auto) + constexpr auto sum_error() const & noexcept -> decltype(auto) requires(some_sum) { return *this; } - auto sum_error() && -> decltype(auto) + constexpr auto sum_error() && noexcept -> decltype(auto) requires(some_sum) { return ::std::move(*this); } - auto sum_error() const && -> decltype(auto) + constexpr auto sum_error() const && noexcept -> decltype(auto) requires(some_sum) { return ::std::move(*this); @@ -1022,11 +1040,16 @@ template class expected : private detail::_expected_ba } }; // Lifts for sum transformation functions -[[nodiscard]] constexpr auto sum_value(some_expected_non_void auto &&src) -> decltype(auto) +[[nodiscard]] constexpr auto sum_value(some_expected_non_void auto &&src) noexcept(noexcept(FWD(src).sum_value())) + -> decltype(auto) { return FWD(src).sum_value(); } -[[nodiscard]] constexpr auto sum_error(some_expected auto &&src) -> decltype(auto) { return FWD(src).sum_error(); } +[[nodiscard]] constexpr auto sum_error(some_expected auto &&src) noexcept(noexcept(FWD(src).sum_error())) + -> decltype(auto) +{ + return FWD(src).sum_error(); +} // 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) diff --git a/include/fn/optional.hpp b/include/fn/optional.hpp index f0937a97..c9980f91 100644 --- a/include/fn/optional.hpp +++ b/include/fn/optional.hpp @@ -477,8 +477,11 @@ template class optional : private detail::_optional_base { // NO return _base::_transform(::std::move(*this), FWD(f)); } - // Convert to graded monad - auto sum_value() const & -> optional> + // Convert to graded monad. The lifting overloads wrap the value in a sum and that sum in the + // result, so they weigh both; the ones whose value type already is a sum only return *this. + constexpr auto sum_value() const & noexcept(::std::is_nothrow_constructible_v, value_type const &> + && ::std::is_nothrow_move_constructible_v>) // extension + -> optional> requires(not some_sum) { using type = optional>; @@ -487,7 +490,9 @@ template class optional : private detail::_optional_base { // NO else return type{::std::nullopt}; } - auto sum_value() && -> optional> + constexpr auto sum_value() && noexcept(::std::is_nothrow_constructible_v, value_type> + && ::std::is_nothrow_move_constructible_v>) // extension + -> optional> requires(not some_sum) { using type = optional>; @@ -496,22 +501,22 @@ template class optional : private detail::_optional_base { // NO else return type{::std::nullopt}; } - auto sum_value() & -> decltype(auto) + constexpr auto sum_value() & noexcept -> decltype(auto) requires(some_sum) { return *this; } - auto sum_value() const & -> decltype(auto) + constexpr auto sum_value() const & noexcept -> decltype(auto) requires(some_sum) { return *this; } - auto sum_value() && -> decltype(auto) + constexpr auto sum_value() && noexcept -> decltype(auto) requires(some_sum) { return ::std::move(*this); } - auto sum_value() const && -> decltype(auto) + constexpr auto sum_value() const && noexcept -> decltype(auto) requires(some_sum) { return ::std::move(*this); @@ -880,7 +885,11 @@ constexpr optional make_optional(::std::initializer_list il, Args &&...arg } // Lifts for sum transformation functions -[[nodiscard]] constexpr auto sum_value(some_optional auto &&src) -> decltype(auto) { return FWD(src).sum_value(); } +[[nodiscard]] constexpr auto sum_value(some_optional auto &&src) noexcept(noexcept(FWD(src).sum_value())) + -> decltype(auto) +{ + return FWD(src).sum_value(); +} template [[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) noexcept { diff --git a/tests/fn/expected.cpp b/tests/fn/expected.cpp index bf11fa86..61b65d69 100644 --- a/tests/fn/expected.cpp +++ b/tests/fn/expected.cpp @@ -232,6 +232,48 @@ TEST_CASE("graded monad", "[expected][sum][graded][and_then][or_else][sum_value] static_assert(std::is_same_v); } + WHEN("constexpr") + { + static_assert([] { + fn::expected const a{::fn::unexpect, Unknown}; + return a.sum_error().error() == fn::sum{Unknown}; + }()); + static_assert([] { return fn::expected{12}.sum_error().value() == 12; }()); + static_assert([] { return fn::expected{12}.sum_value().value() == fn::sum{12}; }()); + static_assert([] { + fn::expected const a{::fn::unexpect, Unknown}; + return a.sum_error().error() == fn::sum{Unknown}; + }()); + static_assert([] { + fn::expected a{12}; + return fn::sum_value(a).value() == fn::sum{12}; + }()); + SUCCEED(); + } + + WHEN("noexcept") + { + using S = fn::expected>; // error already a sum + using V = fn::expected, Error>; // value already a sum + + // the overloads whose side already is a sum only return *this + static_assert(noexcept(std::declval().sum_error())); + static_assert(noexcept(std::declval().sum_error())); + static_assert(noexcept(std::declval().sum_value())); + static_assert(noexcept(std::declval().sum_value())); + static_assert(noexcept(fn::sum_error(std::declval()))); + static_assert(noexcept(fn::sum_value(std::declval()))); + + // a lifting overload wraps one side in a sum and relocates the other, so it weighs both. sum's + // own value constructor carries no noexcept specifier yet (#280), so these are conservatively + // false, and sharpen when it does + static_assert(not noexcept(std::declval &>().sum_error())); + static_assert(not noexcept(std::declval &&>().sum_value())); + static_assert(not noexcept(std::declval &>().sum_error())); + static_assert(not noexcept(fn::sum_error(std::declval &>()))); + SUCCEED(); + } + WHEN("sum_value from sum") { using T = fn::expected, Error>; diff --git a/tests/fn/optional.cpp b/tests/fn/optional.cpp index b060c27e..524e14ca 100644 --- a/tests/fn/optional.cpp +++ b/tests/fn/optional.cpp @@ -79,6 +79,42 @@ TEST_CASE("optional graded monad", "[optional][sum][graded][or_else][sum_value]" } static_assert(std::is_same_v>>); + + WHEN("constexpr") + { + static_assert([] { + fn::optional const a{12}; + return a.sum_value().value() == fn::sum{12}; + }()); + static_assert([] { return fn::optional{12}.sum_value().value() == fn::sum{12}; }()); + static_assert([] { return not fn::optional{std::nullopt}.sum_value().has_value(); }()); + static_assert([] { + fn::optional a{12}; + return fn::sum_value(a).value() == fn::sum{12}; + }()); + SUCCEED(); + } + } + + WHEN("noexcept") + { + using T = fn::optional; + using S = fn::optional>; + + // the sum-valued overloads only return *this + static_assert(noexcept(std::declval().sum_value())); + static_assert(noexcept(std::declval().sum_value())); + static_assert(noexcept(std::declval().sum_value())); + static_assert(noexcept(std::declval().sum_value())); + static_assert(noexcept(fn::sum_value(std::declval()))); + + // the lifting overloads wrap the value in a sum, so they weigh that construction: sum's own + // value constructor carries no noexcept specifier yet (#280), so they are conservatively false + // even for int, and sharpen when it does + static_assert(not noexcept(std::declval().sum_value())); + static_assert(not noexcept(std::declval().sum_value())); + static_assert(not noexcept(fn::sum_value(std::declval()))); + SUCCEED(); } WHEN("or_else")