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
7 changes: 7 additions & 0 deletions include/fn/choice.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,13 @@ struct choice<Ts...> : sum<Ts...> {
constexpr choice(choice &&other) = default;
constexpr ~choice() = default;

// Declared because the move constructor above would otherwise delete the implicit copy assignment
// and suppress the implicit move assignment. Defaulted, so both inherit the base sum's - its
// constraints, its strong guarantee, and its computed noexcept (which an explicit one here would
// contradict, and thereby delete).
constexpr choice &operator=(choice const &other) = default;
constexpr choice &operator=(choice &&other) = default;

/**
* @brief TODO
*
Expand Down
70 changes: 70 additions & 0 deletions include/fn/sum.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <fn/detail/variadic_union.hpp>
#include <fn/functional.hpp>

#include <memory>
#include <type_traits>
#include <utility>

Expand Down Expand Up @@ -372,6 +373,75 @@
});
}

// Replaces the alternative in hand with a `T` built from `args`, mirroring reinit-expected
// ([expected.object.assign]): a sum always holds one of its alternatives - there is no valueless
// state to fall back on - so the one it holds is destroyed only once its replacement is certain.
// Which arm applies is decided per alternative, not for the sum as a whole: a nothrow-copyable one
// is built straight over the old, and one whose copy can throw is copied into a temporary first,
// where only its (nothrow) move goes near the storage.
//
// The standard's third arm - snapshot the old alternative, roll back on throw - has nothing to
// offer here, which is why the constraints below leave no room for it. It would exist for an
// alternative that can neither be built nor moved without throwing; but every alternative is also
// a possible OLD alternative (one is assigned over itself whenever the sum already holds it), and
// snapshotting that same type would throw in turn. So there is no safe path, and such an
// alternative is constrained away rather than half-served.
template <typename T, typename... Args>
constexpr void _reinit(Args &&...args) noexcept(detail::_nothrow_initializable<T, Args...>)
requires has_type<T> && detail::_initializable<T, Args...>
&& (detail::_nothrow_initializable<T, Args...> || ::std::is_nothrow_move_constructible_v<T>)
{
if constexpr (detail::_nothrow_initializable<T, Args...>) {
::std::destroy_at(this);
Comment thread
Bronek marked this conversation as resolved.
::std::construct_at(this, ::std::in_place_type<T>, FWD(args)...);
} else {
T tmp{FWD(args)...}; // may throw, and the storage is untouched until it cannot
::std::destroy_at(this);
::std::construct_at(this, ::std::in_place_type<T>, ::std::move(tmp));

Check warning on line 400 in include/fn/sum.hpp

View check run for this annotation

Codecov / codecov/patch

include/fn/sum.hpp#L399-L400

Added lines #L399 - L400 were not covered by tests
}
}

/**
* @brief Copy assignment, with the strong exception guarantee
*
* @param other TODO
* @return TODO
*/
// The alternative that changes is CONSTRUCTED, never assigned to: assignment asks nothing of `Ts`
// beyond what construction already asks, so a type with no assignment operator at all is still
// assignable through the sum.
constexpr sum &operator=(sum const &other) noexcept((... && ::std::is_nothrow_copy_constructible_v<Ts>))
requires(...
&& (::std::is_copy_constructible_v<Ts>
&& (::std::is_nothrow_copy_constructible_v<Ts> || ::std::is_nothrow_move_constructible_v<Ts>)))
{
if (this != &other) {
detail::invoke_type_variadic_union<void, data_t>( //
other.data, other.index,
[this]<typename T>(::std::in_place_type_t<T>, auto const &v) { this->template _reinit<T>(v); });
}
return *this;
}

/**
* @brief Move assignment, with the strong exception guarantee
*
* @param other TODO
* @return TODO
*/
// Moving is offered only where it cannot throw, for the reason given on _reinit - and where it can,
// a nothrow-copyable sum is still assignable from an rvalue, by copy.
constexpr sum &operator=(sum &&other) noexcept
requires(... && ::std::is_nothrow_move_constructible_v<Ts>)
{
if (this != &other) {
detail::invoke_type_variadic_union<void, data_t>( //
::std::move(other).data, other.index,
[this]<typename T>(::std::in_place_type_t<T>, auto &&v) { this->template _reinit<T>(FWD(v)); });
}
return *this;
}

/**
* @brief TODO
*
Expand Down
38 changes: 38 additions & 0 deletions tests/fn/choice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

#include <catch2/catch_all.hpp>

#include <string>
#include <type_traits>
#include <utility>

namespace {
Expand Down Expand Up @@ -783,3 +785,39 @@ TEST_CASE("choice transform", "[choice][transform]")
}
}
}

TEST_CASE("choice assignment", "[choice][assignment]")
{
using fn::choice;

// choice adds no state of its own, so its assignment is the base sum's: same strong guarantee,
// same constraints, same noexcept - it only has to be declared, since choice's move constructor
// would otherwise delete the implicit copy assignment and suppress the implicit move assignment
static_assert(std::is_copy_assignable_v<choice<bool, int>>);
static_assert(std::is_move_assignable_v<choice<bool, int>>);
static_assert(std::is_nothrow_copy_assignable_v<choice<bool, int>>);
static_assert(not std::is_nothrow_copy_assignable_v<choice<std::string>>);
static_assert(std::is_nothrow_move_assignable_v<choice<std::string>>);

WHEN("the alternative changes")
{
choice<bool, int> a{42};
choice<bool, int> const b{true};
a = b;
CHECK(a == choice{true});
CHECK(a.has_value(std::in_place_type<bool>));

a = choice<bool, int>{12};
CHECK(a == choice{12});
}

WHEN("constexpr")
{
static_assert([] {
choice<bool, int> a{42};
a = choice<bool, int>{true};
return a == choice{true};
}());
SUCCEED();
}
}
22 changes: 22 additions & 0 deletions tests/fn/expected.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,28 @@ TEST_CASE("graded monad", "[expected][sum][graded][and_then][or_else][sum_value]
}
}

WHEN("assignment")
{
// a graded monad is assignable because its summed error is: without sum::operator= the whole
// expected<T, sum<...>> would not be assignable at all
using T = fn::expected<int, fn::sum_for<Error, bool>>; // Error is an enum: the order is per-platform
static_assert(std::is_copy_assignable_v<T>);
static_assert(std::is_move_assignable_v<T>);

T a{12};
T const b{::fn::unexpect, fn::sum{FileNotFound}};
a = b;
CHECK(a.error() == fn::sum{FileNotFound});
a = T{42};
CHECK(a.value() == 42);

static_assert([] {
T a{12};
a = T{::fn::unexpect, fn::sum{true}};
return a.error() == fn::sum{true};
}());
}

WHEN("noexcept")
{
// the widening arms relocate BOTH sides into the summed result - self's, and the callback's -
Expand Down
Loading