Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reuse any_sender storage when moving to a unique_any_sender #844

Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,18 @@ namespace pika::detail {
return ∅
}
#endif
template <typename Base, std::size_t EmbeddedStorageSize,
std::size_t AlignmentSize = sizeof(void*)>
class copyable_sbo_storage;

template <typename Base, std::size_t EmbeddedStorageSize,
std::size_t AlignmentSize = sizeof(void*)>
class movable_sbo_storage
{
protected:
using base_type = Base;
#if defined(PIKA_DETAIL_ENABLE_ANY_SENDER_SBO)
static constexpr std::size_t embedded_storage_size = EmbeddedStorageSize;
static constexpr std::size_t alignment_size = AlignmentSize;
#endif

// The union has two members:
// - embedded_storage: Embedded storage size array used for types that
Expand Down Expand Up @@ -180,6 +181,35 @@ namespace pika::detail {
}
}

template <typename T>
void move_assign(copyable_sbo_storage<T, embedded_storage_size, alignment_size>&& other)
{
static_assert(std::is_base_of_v<base_type, T>);

PIKA_ASSERT(static_cast<void*>(&other) != static_cast<void*>(this));
PIKA_ASSERT(empty());

if (!other.empty())
{
#if defined(PIKA_DETAIL_ENABLE_ANY_SENDER_SBO)
if (other.using_embedded_storage())
{
auto p = reinterpret_cast<base_type*>(&embedded_storage);
other.get().move_into(p);
object = p;
}
else
#endif
{
heap_storage = other.heap_storage;
other.heap_storage = nullptr;
object = heap_storage;
}

other.reset_vtable();
}
}

public:
movable_sbo_storage() = default;

Expand All @@ -190,6 +220,15 @@ namespace pika::detail {

movable_sbo_storage(movable_sbo_storage&& other) { move_assign(PIKA_MOVE(other)); }

template <typename T>
explicit movable_sbo_storage(
copyable_sbo_storage<T, embedded_storage_size, alignment_size>&& other)
{
static_assert(std::is_base_of_v<base_type, T>);

move_assign(PIKA_MOVE(other));
}

movable_sbo_storage& operator=(movable_sbo_storage&& other)
{
if (&other != this)
Expand All @@ -201,6 +240,21 @@ namespace pika::detail {
return *this;
}

template <typename T>
movable_sbo_storage&
operator=(copyable_sbo_storage<T, embedded_storage_size, alignment_size>&& other)
{
static_assert(std::is_base_of_v<base_type, T>);

if (static_cast<void*>(&other) != static_cast<void*>(this))
{
if (!empty()) { release(); }

move_assign(PIKA_MOVE(other));
}
return *this;
}

movable_sbo_storage(movable_sbo_storage const&) = delete;
movable_sbo_storage& operator=(movable_sbo_storage const&) = delete;

Expand Down Expand Up @@ -236,11 +290,13 @@ namespace pika::detail {
}
};

template <typename Base, std::size_t EmbeddedStorageSize,
std::size_t AlignmentSize = sizeof(void*)>
template <typename Base, std::size_t EmbeddedStorageSize, std::size_t AlignmentSize>
class copyable_sbo_storage
: public movable_sbo_storage<Base, EmbeddedStorageSize, AlignmentSize>
{
template <typename Base2, std::size_t EmbeddedStorageSize2, std::size_t AlignmentSize2>
friend class movable_sbo_storage;

using storage_base_type = movable_sbo_storage<Base, EmbeddedStorageSize, AlignmentSize>;

using typename storage_base_type::base_type;
Expand Down Expand Up @@ -658,6 +714,9 @@ namespace pika::execution::experimental {
} // namespace detail
#endif

template <typename... Ts>
class any_sender;

template <typename... Ts>
class unique_any_sender
#if !defined(PIKA_HAVE_CXX20_TRIVIAL_VIRTUAL_DESTRUCTOR)
Expand Down Expand Up @@ -698,6 +757,20 @@ namespace pika::execution::experimental {
unique_any_sender& operator=(unique_any_sender&&) = default;
unique_any_sender& operator=(unique_any_sender const&) = delete;

// cppcheck-suppress noExplicitConstructor
unique_any_sender(any_sender<Ts...>&& other)
: storage(PIKA_MOVE(other.storage))
{
other.reset();
}

unique_any_sender& operator=(any_sender<Ts...>&& other)
{
storage = PIKA_MOVE(other.storage);
other.reset();
return *this;
};

template <template <typename...> class Tuple, template <typename...> class Variant>
using value_types = Variant<Tuple<Ts...>>;

Expand Down Expand Up @@ -767,6 +840,8 @@ namespace pika::execution::experimental {

storage_type storage{};

friend unique_any_sender<Ts...>;

public:
using is_sender = void;
any_sender() = default;
Expand Down
98 changes: 98 additions & 0 deletions libs/pika/execution_base/tests/unit/any_sender.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,85 @@ void test_const_reference()
PIKA_TEST(true);
}

template <template <typename...> typename Sender, typename... Ts, typename F>
void test_any_sender_to_unique_any_sender(F&& f, Ts&&... ts)
{
static_assert(std::is_copy_constructible_v<Sender<Ts...>>,
"This test requires the sender to be copy constructible.");

Sender<std::decay_t<Ts>...> s{std::forward<Ts>(ts)...};

ex::any_sender<std::decay_t<Ts>...> as1{s};
ex::any_sender<std::decay_t<Ts>...> as2{s};

ex::unique_any_sender<std::decay_t<Ts>...> as3{as1};
ex::unique_any_sender<std::decay_t<Ts>...> as4;
as4 = as2;

ex::unique_any_sender<std::decay_t<Ts>...> as5{std::move(as1)};
ex::unique_any_sender<std::decay_t<Ts>...> as6;
as6 = std::move(as2);

PIKA_TEST(as1.empty());
PIKA_TEST(as2.empty());
PIKA_TEST(as3);
PIKA_TEST(as4);
PIKA_TEST(as5);
PIKA_TEST(as6);

// We expect set_value to be called here for as3 to as6
auto check_set_value = [&](auto&& s) {
std::atomic<bool> set_value_called = false;
auto os =
ex::connect(std::forward<decltype(s)>(s), callback_receiver<F>{f, set_value_called});
ex::start(os);
PIKA_TEST(set_value_called);
// NOLINTNEXTLINE(bugprone-use-after-move)
PIKA_TEST(s.empty());
PIKA_TEST(!s);
};

check_set_value(std::move(as3));
check_set_value(std::move(as4));
check_set_value(std::move(as5));
check_set_value(std::move(as6));

// All senders have been moved now so we expect exceptions here
auto check_exception = [&](auto&& s) {
std::atomic<bool> set_value_called{false};
try
{
auto os = ex::connect(
// NOLINTNEXTLINE(bugprone-use-after-move)
std::forward<decltype(s)>(s), callback_receiver<F>{f, set_value_called});
PIKA_TEST(false);
ex::start(os);
}
catch (pika::exception const& e)
{
PIKA_TEST_EQ(e.get_error(), pika::error::bad_function_call);
}
catch (...)
{
PIKA_TEST(false);
}
PIKA_TEST(!set_value_called);
};

// NOLINTNEXTLINE(bugprone-use-after-move)
check_exception(std::move(as1));
// NOLINTNEXTLINE(bugprone-use-after-move)
check_exception(std::move(as2));
// NOLINTNEXTLINE(bugprone-use-after-move)
check_exception(std::move(as3));
// NOLINTNEXTLINE(bugprone-use-after-move)
check_exception(std::move(as4));
// NOLINTNEXTLINE(bugprone-use-after-move)
check_exception(std::move(as5));
// NOLINTNEXTLINE(bugprone-use-after-move)
check_exception(std::move(as6));
}

int main()
{
// We can only wrap copyable senders in any_sender
Expand Down Expand Up @@ -812,5 +891,24 @@ int main()
// Test using {unique_,}any_sender with a just sender of a const reference
test_const_reference();

// Test construction of unique_any_sender from any_sender
test_any_sender_to_unique_any_sender<sender>([] {});
test_any_sender_to_unique_any_sender<sender, int>([](int x) { PIKA_TEST_EQ(x, 42); }, 42);
test_any_sender_to_unique_any_sender<sender, int, double>(
[](int x, double y) {
PIKA_TEST_EQ(x, 42);
PIKA_TEST_EQ(y, 3.14);
},
42, 3.14);

test_any_sender_to_unique_any_sender<large_sender>([] {});
test_any_sender_to_unique_any_sender<large_sender, int>([](int x) { PIKA_TEST_EQ(x, 42); }, 42);
test_any_sender_to_unique_any_sender<large_sender, int, double>(
[](int x, double y) {
PIKA_TEST_EQ(x, 42);
PIKA_TEST_EQ(y, 3.14);
},
42, 3.14);

return 0;
}