Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 45 additions & 22 deletions include/fn/expected.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -632,8 +632,12 @@ template <typename T, typename Err> class expected : private detail::_expected_b
return _base::_transform_error(::std::move(*this), FWD(f));
}

// Convert to graded monad
auto sum_error() const & -> expected<value_type, sum<error_type>>
// 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<value_type, value_type const &>
&& ::std::is_nothrow_constructible_v<sum<error_type>, error_type const &>
&& ::std::is_nothrow_move_constructible_v<sum<error_type>>) // extension
-> expected<value_type, sum<error_type>>
requires(not some_sum<error_type>)
{
using type = expected<value_type, sum<error_type>>;
Expand All @@ -642,7 +646,10 @@ template <typename T, typename Err> class expected : private detail::_expected_b
else
return type{::fn::unexpect, sum<error_type>(this->error())};
}
auto sum_error() && -> expected<value_type, sum<error_type>>
constexpr auto sum_error() && noexcept(::std::is_nothrow_constructible_v<value_type, value_type>
&& ::std::is_nothrow_constructible_v<sum<error_type>, error_type>
&& ::std::is_nothrow_move_constructible_v<sum<error_type>>) // extension
-> expected<value_type, sum<error_type>>
requires(not some_sum<error_type>)
{
using type = expected<value_type, sum<error_type>>;
Expand All @@ -651,28 +658,32 @@ template <typename T, typename Err> class expected : private detail::_expected_b
else
return type{::fn::unexpect, sum<error_type>(::std::move(*this).error())};
}
auto sum_error() & -> decltype(auto)
constexpr auto sum_error() & noexcept -> decltype(auto)
requires(some_sum<error_type>)
{
return *this;
}
auto sum_error() const & -> decltype(auto)
constexpr auto sum_error() const & noexcept -> decltype(auto)
requires(some_sum<error_type>)
{
return *this;
}
auto sum_error() && -> decltype(auto)
constexpr auto sum_error() && noexcept -> decltype(auto)
requires(some_sum<error_type>)
{
return ::std::move(*this);
}
auto sum_error() const && -> decltype(auto)
constexpr auto sum_error() const && noexcept -> decltype(auto)
requires(some_sum<error_type>)
{
return ::std::move(*this);
}

auto sum_value() const & -> expected<sum<value_type>, error_type>
constexpr auto
sum_value() const & noexcept(::std::is_nothrow_constructible_v<sum<value_type>, value_type const &>
&& ::std::is_nothrow_move_constructible_v<sum<value_type>>
&& ::std::is_nothrow_constructible_v<error_type, error_type const &>) // extension
-> expected<sum<value_type>, error_type>
requires(not some_sum<value_type>)
{
using type = expected<sum<value_type>, error_type>;
Expand All @@ -681,7 +692,10 @@ template <typename T, typename Err> class expected : private detail::_expected_b
else
return type{::fn::unexpect, this->error()};
}
auto sum_value() && -> expected<sum<value_type>, error_type>
constexpr auto sum_value() && noexcept(::std::is_nothrow_constructible_v<sum<value_type>, value_type>
&& ::std::is_nothrow_move_constructible_v<sum<value_type>>
&& ::std::is_nothrow_constructible_v<error_type, error_type>) // extension
-> expected<sum<value_type>, error_type>
requires(not some_sum<value_type>)
{
using type = expected<sum<value_type>, error_type>;
Expand All @@ -690,22 +704,22 @@ template <typename T, typename Err> 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<value_type>)
{
return *this;
}
auto sum_value() const & -> decltype(auto)
constexpr auto sum_value() const & noexcept -> decltype(auto)
requires(some_sum<value_type>)
{
return *this;
}
auto sum_value() && -> decltype(auto)
constexpr auto sum_value() && noexcept -> decltype(auto)
requires(some_sum<value_type>)
{
return ::std::move(*this);
}
auto sum_value() const && -> decltype(auto)
constexpr auto sum_value() const && noexcept -> decltype(auto)
requires(some_sum<value_type>)
{
return ::std::move(*this);
Expand Down Expand Up @@ -971,8 +985,10 @@ template <typename Err> class expected<void, Err> : private detail::_expected_ba
return _base::_transform_error(::std::move(*this), FWD(f));
}

// Convert to graded monad
auto sum_error() const & -> expected<value_type, sum<error_type>>
// 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<sum<error_type>, error_type const &>
&& ::std::is_nothrow_move_constructible_v<sum<error_type>>) // extension
-> expected<value_type, sum<error_type>>
requires(not some_sum<error_type>)
{
using type = expected<value_type, sum<error_type>>;
Expand All @@ -981,7 +997,9 @@ template <typename Err> class expected<void, Err> : private detail::_expected_ba
else
return type{::fn::unexpect, sum<error_type>(this->error())};
}
auto sum_error() && -> expected<value_type, sum<error_type>>
constexpr auto sum_error() && noexcept(::std::is_nothrow_constructible_v<sum<error_type>, error_type>
&& ::std::is_nothrow_move_constructible_v<sum<error_type>>) // extension
-> expected<value_type, sum<error_type>>
requires(not some_sum<error_type>)
{
using type = expected<value_type, sum<error_type>>;
Expand All @@ -990,22 +1008,22 @@ template <typename Err> class expected<void, Err> : private detail::_expected_ba
else
return type{::fn::unexpect, sum<error_type>(::std::move(*this).error())};
}
auto sum_error() & -> decltype(auto)
constexpr auto sum_error() & noexcept -> decltype(auto)
requires(some_sum<error_type>)
{
return *this;
}
auto sum_error() const & -> decltype(auto)
constexpr auto sum_error() const & noexcept -> decltype(auto)
requires(some_sum<error_type>)
{
return *this;
}
auto sum_error() && -> decltype(auto)
constexpr auto sum_error() && noexcept -> decltype(auto)
requires(some_sum<error_type>)
{
return ::std::move(*this);
}
auto sum_error() const && -> decltype(auto)
constexpr auto sum_error() const && noexcept -> decltype(auto)
requires(some_sum<error_type>)
{
return ::std::move(*this);
Expand All @@ -1022,11 +1040,16 @@ template <typename Err> class expected<void, Err> : 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<void, ...>, we do not produce expected<pack<...>, ...>
// Instead just elide void and carry non-void (or elide both voids if that's what we get)
Expand Down
25 changes: 17 additions & 8 deletions include/fn/optional.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -477,8 +477,11 @@ template <typename T> class optional : private detail::_optional_base<T> { // NO
return _base::_transform(::std::move(*this), FWD(f));
}

// Convert to graded monad
auto sum_value() const & -> optional<sum<value_type>>
// 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<sum<value_type>, value_type const &>
&& ::std::is_nothrow_move_constructible_v<sum<value_type>>) // extension
-> optional<sum<value_type>>
requires(not some_sum<value_type>)
{
using type = optional<sum<value_type>>;
Expand All @@ -487,7 +490,9 @@ template <typename T> class optional : private detail::_optional_base<T> { // NO
else
return type{::std::nullopt};
}
auto sum_value() && -> optional<sum<value_type>>
constexpr auto sum_value() && noexcept(::std::is_nothrow_constructible_v<sum<value_type>, value_type>
&& ::std::is_nothrow_move_constructible_v<sum<value_type>>) // extension
-> optional<sum<value_type>>
requires(not some_sum<value_type>)
{
using type = optional<sum<value_type>>;
Expand All @@ -496,22 +501,22 @@ template <typename T> class optional : private detail::_optional_base<T> { // NO
else
return type{::std::nullopt};
}
auto sum_value() & -> decltype(auto)
constexpr auto sum_value() & noexcept -> decltype(auto)
requires(some_sum<value_type>)
{
return *this;
}
auto sum_value() const & -> decltype(auto)
constexpr auto sum_value() const & noexcept -> decltype(auto)
requires(some_sum<value_type>)
{
return *this;
}
auto sum_value() && -> decltype(auto)
constexpr auto sum_value() && noexcept -> decltype(auto)
requires(some_sum<value_type>)
{
return ::std::move(*this);
}
auto sum_value() const && -> decltype(auto)
constexpr auto sum_value() const && noexcept -> decltype(auto)
requires(some_sum<value_type>)
{
return ::std::move(*this);
Expand Down Expand Up @@ -880,7 +885,11 @@ constexpr optional<T> make_optional(::std::initializer_list<U> 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 <some_optional Lh, some_optional Rh> [[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) noexcept
{
Expand Down
42 changes: 42 additions & 0 deletions tests/fn/expected.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,48 @@ TEST_CASE("graded monad", "[expected][sum][graded][and_then][or_else][sum_value]
static_assert(std::is_same_v<decltype(fn::sum_error(s)), T &>);
}

WHEN("constexpr")
{
static_assert([] {
fn::expected<int, Error> const a{::fn::unexpect, Unknown};
return a.sum_error().error() == fn::sum{Unknown};
}());
static_assert([] { return fn::expected<int, Error>{12}.sum_error().value() == 12; }());
static_assert([] { return fn::expected<int, Error>{12}.sum_value().value() == fn::sum{12}; }());
static_assert([] {
fn::expected<void, Error> const a{::fn::unexpect, Unknown};
return a.sum_error().error() == fn::sum{Unknown};
}());
static_assert([] {
fn::expected<int, Error> a{12};
return fn::sum_value(a).value() == fn::sum{12};
}());
SUCCEED();

@augmentcode augmentcode Bot Jul 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests/fn/expected.cpp:251: The new WHEN("constexpr")/WHEN("noexcept") sections only use static_assert plus SUCCEED(), so they don’t actually validate the behavior at runtime. Consider adding runtime CHECK/CHECK_FALSE counterparts (even if they mirror the constant expressions) to match the repo’s “runtime + constexpr twin” testing convention.

Severity: low

Other Locations
  • tests/fn/optional.cpp:95

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

}

WHEN("noexcept")
{
using S = fn::expected<int, fn::sum<Error>>; // error already a sum
using V = fn::expected<fn::sum<int>, Error>; // value already a sum

// the overloads whose side already is a sum only return *this
static_assert(noexcept(std::declval<S &>().sum_error()));
static_assert(noexcept(std::declval<S const &&>().sum_error()));
static_assert(noexcept(std::declval<V &>().sum_value()));
static_assert(noexcept(std::declval<V const &&>().sum_value()));
static_assert(noexcept(fn::sum_error(std::declval<S &>())));
static_assert(noexcept(fn::sum_value(std::declval<V &>())));

// 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<fn::expected<int, Error> &>().sum_error()));
static_assert(not noexcept(std::declval<fn::expected<int, Error> &&>().sum_value()));
static_assert(not noexcept(std::declval<fn::expected<void, Error> &>().sum_error()));
static_assert(not noexcept(fn::sum_error(std::declval<fn::expected<int, Error> &>())));
SUCCEED();
}

WHEN("sum_value from sum")
{
using T = fn::expected<fn::sum<int>, Error>;
Expand Down
36 changes: 36 additions & 0 deletions tests/fn/optional.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,42 @@ TEST_CASE("optional graded monad", "[optional][sum][graded][or_else][sum_value]"
}

static_assert(std::is_same_v<decltype(fn::sum_value(s)), fn::optional<fn::sum<int>>>);

WHEN("constexpr")
{
static_assert([] {
fn::optional<int> const a{12};
return a.sum_value().value() == fn::sum{12};
}());
static_assert([] { return fn::optional<int>{12}.sum_value().value() == fn::sum{12}; }());
static_assert([] { return not fn::optional<int>{std::nullopt}.sum_value().has_value(); }());
static_assert([] {
fn::optional<int> a{12};
return fn::sum_value(a).value() == fn::sum{12};
}());
SUCCEED();
}
}

WHEN("noexcept")
{
using T = fn::optional<int>;
using S = fn::optional<fn::sum<int>>;

// the sum-valued overloads only return *this
static_assert(noexcept(std::declval<S &>().sum_value()));
static_assert(noexcept(std::declval<S const &>().sum_value()));
static_assert(noexcept(std::declval<S &&>().sum_value()));
static_assert(noexcept(std::declval<S const &&>().sum_value()));
static_assert(noexcept(fn::sum_value(std::declval<S &>())));

// 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<T &>().sum_value()));
static_assert(not noexcept(std::declval<T &&>().sum_value()));
static_assert(not noexcept(fn::sum_value(std::declval<T &>())));
SUCCEED();
}

WHEN("or_else")
Expand Down