Skip to content
Open
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
43 changes: 43 additions & 0 deletions include/stdexec/__detail/__receivers.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,49 @@ namespace STDEXEC
struct _THE_RECEIVER_DOES_NOT_ACCEPT_ALL_OF_THE_SENDERS_COMPLETION_SIGNALS_
{};

//! @brief A receiver that can be reconstructed from its child operation state.
//!
//! `inlinable_receiver<R, ChildOp>` refines @c receiver and requires the
//! decayed receiver type to provide a class-level `make_receiver_for`
//! function. The function is called with a pointer to the child operation
//! state and must be non-throwing and return exactly the decayed receiver
//! type:
//!
//! @code{.cpp}
//! struct my_receiver {
//! using receiver_concept = stdexec::receiver_tag;
//!
//! template <class ChildOp>
//! static my_receiver make_receiver_for(ChildOp*) noexcept;
//!
//! // set_value, set_error, set_stopped, and get_env ...
//! };
//! @endcode
//!
//! An operation state that uses such a receiver can recreate it on demand
//! with `R::make_receiver_for(this)` instead of storing a receiver object.
//! This can reduce the size of nested operation states when the receiver's
//! state is recoverable from the operation-state address. `ChildOp` only
//! appears as a pointer parameter, so it may be incomplete when the concept
//! is checked in an operation-state definition.
//!
//! The concept checks the syntactic protocol only. For a receiver object
//! `rcvr` connected to an operation state `op`, the semantic requirement is
//! that `R::make_receiver_for(std::addressof(op))` recreate a receiver equal
//! to `rcvr`.
//!
//! See [exec.recv.concepts] in the C++26 working draft.
//!
//! @see stdexec::receiver
//! @see stdexec::operation_state
STDEXEC_MODULE_EXPORT
template <class _Receiver, class _ChildOp>
concept inlinable_receiver = receiver<_Receiver> && requires(_ChildOp *__child_op) {
{
std::remove_cvref_t<_Receiver>::make_receiver_for(__child_op)
} noexcept -> __std::same_as<std::remove_cvref_t<_Receiver>>;
};

namespace __detail
{
template <class _Receiver, class _Tag, class... _Args>
Expand Down
286 changes: 252 additions & 34 deletions test/stdexec/concepts/test_concepts_receiver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@

#include <test_common/receivers.hpp>

#include <concepts>
#include <cstddef>
#include <memory>
#include <new>
#include <type_traits>
#include <utility>

namespace ex = STDEXEC;

struct recv_no_set_value
Expand Down Expand Up @@ -125,63 +132,274 @@ struct recv_non_movable
}
};

template <class _ParentOp, class _Env>
struct test_basic_receiver
{
using receiver_concept = ex::receiver_tag;

template <class _ChildOp>
static test_basic_receiver make_receiver_for(_ChildOp *__child) noexcept
{
// `_ChildOp` is the first non-static data member of `_ParentOp`.
auto const address = reinterpret_cast<void *>(__child);
// NOTE: Actually, it's still UB. Under the current standard, pointer conversion/arithmetic cannot achieve
// this. A truly well-defined approach will likely have to wait until proposals like P3407 are adopted into the
// standard.
return test_basic_receiver{reinterpret_cast<_ParentOp *>(address)};
}

template <class... _Args>
void set_value(_Args &&...__args) noexcept
{
ex::set_value(std::move(__parent->get_receiver()), static_cast<_Args &&>(__args)...);
}

template <class _Error>
void set_error(_Error &&__error) noexcept
{
ex::set_error(std::move(__parent->get_receiver()), static_cast<_Error &&>(__error));
}

void set_stopped() noexcept
{
ex::set_stopped(std::move(__parent->get_receiver()));
}

_Env get_env() const noexcept
{
return ex::get_env(__parent->get_receiver());
}

_ParentOp *__parent;
};

template <class _Sender, class _Receiver>
struct test_basic_state
{
using operation_state_concept = ex::operation_state_tag;

using receiver_t = test_basic_receiver<test_basic_state, ex::env_of_t<_Receiver>>;
using child_op_t = ex::connect_result_t<_Sender, receiver_t>;

test_basic_state(_Sender __sender, _Receiver __receiver)
: receiver(std::move(__receiver))
{
// The child operation state should be created after `receiver` constructed
::new (static_cast<void *>(storage))
child_op_t(ex::connect(std::move(__sender), receiver_t{this}));
}

~test_basic_state()
{
std::destroy_at(std::launder(reinterpret_cast<child_op_t *>(storage)));
}

void start() & noexcept
{
ex::start(*std::launder(reinterpret_cast<child_op_t *>(storage)));
}

_Receiver &get_receiver() noexcept
{
return receiver;
}

alignas(child_op_t) std::byte storage[sizeof(child_op_t)];
_Receiver receiver;
};

template <class _Sender, class _Receiver>
requires ex::inlinable_receiver<_Receiver, test_basic_state<_Sender, _Receiver>>
struct test_basic_state<_Sender, _Receiver>
{
using operation_state_concept = ex::operation_state_tag;

using receiver_t = test_basic_receiver<test_basic_state, ex::env_of_t<_Receiver>>;
using child_op_t = ex::connect_result_t<_Sender, receiver_t>;

test_basic_state(_Sender __sender, _Receiver const &)
: child_op(ex::connect(std::move(__sender), receiver_t{this}))
{}

void start() & noexcept
{
ex::start(child_op);
}

_Receiver get_receiver() noexcept
{
return _Receiver::make_receiver_for(this);
}

child_op_t child_op;
};

template <class _Tag, class _Child>
struct test_basic_sender
{
using sender_concept = ex::sender_tag;

_Tag tag;
_Child child;

template <class, class... _Env>
requires ex::sender_in<_Child, _Env...>
static consteval auto get_completion_signatures()
{
return ex::get_completion_signatures<_Child, _Env...>();
}

template <class _Receiver>
auto connect(_Receiver __receiver)
{
return test_basic_state<_Child, _Receiver>{std::move(child), std::move(__receiver)};
}
};

struct test_adaptor : ex::sender_adaptor_closure<test_adaptor>
{
template <class _Sender>
auto operator()(_Sender __sender) const noexcept
{
return test_basic_sender<test_adaptor, _Sender>{*this, std::move(__sender)};
}
};

struct inlinable_state
{};

struct non_inlinable_state
{};

struct normal_receiver
{
using receiver_concept = ex::receiver_tag;

static void set_value(auto const &...) noexcept {}
static void set_error(auto const &) noexcept {}
static void set_stopped() noexcept {}
};

struct inlinable_receiver
{
using receiver_concept = ex::receiver_tag;

static inlinable_receiver make_receiver_for(inlinable_state *) noexcept
{
return {};
}

static normal_receiver make_receiver_for(non_inlinable_state *) noexcept
{
return {};
}

template <class _Child, class _Receiver>
static inlinable_receiver make_receiver_for(test_basic_state<_Child, _Receiver> *) noexcept
{
return {};
}

static void set_value(auto const &...) noexcept {}
static void set_error(auto const &) noexcept {}
static void set_stopped() noexcept {}
};

static_assert(ex::receiver<inlinable_receiver>);

TEST_CASE("receiver types satisfy the receiver concept", "[concepts][receiver]")
{
using namespace empty_recv;

REQUIRE(ex::receiver<recv0>);
REQUIRE(ex::receiver<recv_int>);
REQUIRE(ex::receiver<recv0_ec>);
REQUIRE(ex::receiver<recv_int_ec>);
REQUIRE(ex::receiver<expect_void_receiver<>>);
REQUIRE(ex::receiver<expect_void_receiver_ex>);
REQUIRE(ex::receiver<expect_value_receiver<ex::env<>, int>>);
REQUIRE(ex::receiver<expect_value_receiver<ex::env<>, double>>);
REQUIRE(ex::receiver<expect_stopped_receiver<>>);
REQUIRE(ex::receiver<expect_stopped_receiver_ex<>>);
REQUIRE(ex::receiver<expect_error_receiver<>>);
REQUIRE(ex::receiver<expect_error_receiver_ex<std::error_code>>);
REQUIRE(ex::receiver<logging_receiver>);
STATIC_REQUIRE(ex::receiver<recv0>);
STATIC_REQUIRE(ex::receiver<recv_int>);
STATIC_REQUIRE(ex::receiver<recv0_ec>);
STATIC_REQUIRE(ex::receiver<recv_int_ec>);
STATIC_REQUIRE(ex::receiver<expect_void_receiver<>>);
STATIC_REQUIRE(ex::receiver<expect_void_receiver_ex>);
STATIC_REQUIRE(ex::receiver<expect_value_receiver<ex::env<>, int>>);
STATIC_REQUIRE(ex::receiver<expect_value_receiver<ex::env<>, double>>);
STATIC_REQUIRE(ex::receiver<expect_stopped_receiver<>>);
STATIC_REQUIRE(ex::receiver<expect_stopped_receiver_ex<>>);
STATIC_REQUIRE(ex::receiver<expect_error_receiver<>>);
STATIC_REQUIRE(ex::receiver<expect_error_receiver_ex<std::error_code>>);
STATIC_REQUIRE(ex::receiver<logging_receiver>);
}

TEST_CASE("receiver types satisfy the receiver_of concept", "[concepts][receiver]")
{
using namespace empty_recv;

REQUIRE(ex::receiver_of<recv_int, ex::completion_signatures<ex::set_value_t(int)>>);
REQUIRE(ex::receiver_of<recv0_ec, ex::completion_signatures<ex::set_error_t(std::error_code)>>);
REQUIRE(
STATIC_REQUIRE(ex::receiver_of<recv_int, ex::completion_signatures<ex::set_value_t(int)>>);
STATIC_REQUIRE(
ex::receiver_of<recv0_ec, ex::completion_signatures<ex::set_error_t(std::error_code)>>);
STATIC_REQUIRE(
ex::receiver_of<recv_int_ec, ex::completion_signatures<ex::set_error_t(std::error_code)>>);
REQUIRE(ex::receiver_of<recv_int_ec, ex::completion_signatures<ex::set_value_t(int)>>);
REQUIRE(ex::receiver_of<expect_value_receiver<ex::env<>, int>,
ex::completion_signatures<ex::set_value_t(int)>>);
REQUIRE(ex::receiver_of<expect_value_receiver<ex::env<>, double>,
ex::completion_signatures<ex::set_value_t(double)>>);
REQUIRE(
STATIC_REQUIRE(ex::receiver_of<recv_int_ec, ex::completion_signatures<ex::set_value_t(int)>>);
STATIC_REQUIRE(ex::receiver_of<expect_value_receiver<ex::env<>, int>,
ex::completion_signatures<ex::set_value_t(int)>>);
STATIC_REQUIRE(ex::receiver_of<expect_value_receiver<ex::env<>, double>,
ex::completion_signatures<ex::set_value_t(double)>>);
STATIC_REQUIRE(
ex::receiver_of<expect_stopped_receiver<>, ex::completion_signatures<ex::set_value_t(char)>>);
REQUIRE(ex::receiver_of<expect_stopped_receiver_ex<>,
ex::completion_signatures<ex::set_value_t(char)>>);
REQUIRE(
STATIC_REQUIRE(ex::receiver_of<expect_stopped_receiver_ex<>,
ex::completion_signatures<ex::set_value_t(char)>>);
STATIC_REQUIRE(
ex::receiver_of<expect_error_receiver<>, ex::completion_signatures<ex::set_value_t(char)>>);
REQUIRE(ex::receiver_of<expect_error_receiver_ex<std::error_code>,
ex::completion_signatures<ex::set_value_t(char)>>);
REQUIRE(ex::receiver_of<logging_receiver, ex::completion_signatures<ex::set_value_t(char)>>);
STATIC_REQUIRE(ex::receiver_of<expect_error_receiver_ex<std::error_code>,
ex::completion_signatures<ex::set_value_t(char)>>);
STATIC_REQUIRE(
ex::receiver_of<logging_receiver, ex::completion_signatures<ex::set_value_t(char)>>);
}

TEST_CASE("receiver type w/o set_value models receiver but not receiver_of", "[concepts][receiver]")
{
REQUIRE(ex::receiver<recv_no_set_value>);
REQUIRE(!ex::receiver_of<recv_no_set_value, ex::completion_signatures<ex::set_value_t()>>);
STATIC_REQUIRE(ex::receiver<recv_no_set_value>);
STATIC_REQUIRE(!ex::receiver_of<recv_no_set_value, ex::completion_signatures<ex::set_value_t()>>);
}

TEST_CASE("type with set_value noexcept is a receiver", "[concepts][receiver]")
{
REQUIRE(ex::receiver<recv_set_value_noexcept>);
REQUIRE(ex::receiver_of<recv_set_value_noexcept, ex::completion_signatures<ex::set_value_t()>>);
STATIC_REQUIRE(ex::receiver<recv_set_value_noexcept>);
STATIC_REQUIRE(
ex::receiver_of<recv_set_value_noexcept, ex::completion_signatures<ex::set_value_t()>>);
}

TEST_CASE("non-movable type is not a receiver", "[concepts][receiver]")
{
REQUIRE(!ex::receiver<recv_non_movable>);
REQUIRE(!ex::receiver_of<recv_non_movable, ex::completion_signatures<ex::set_value_t()>>);
STATIC_REQUIRE(!ex::receiver<recv_non_movable>);
STATIC_REQUIRE(!ex::receiver_of<recv_non_movable, ex::completion_signatures<ex::set_value_t()>>);
}

TEST_CASE("inlinable_receiver", "[concepts][receiver]")
{
STATIC_REQUIRE(!ex::inlinable_receiver<inlinable_receiver, int>);
STATIC_REQUIRE(!ex::inlinable_receiver<inlinable_receiver, non_inlinable_state>);
STATIC_REQUIRE(ex::inlinable_receiver<inlinable_receiver, inlinable_state>);

auto sender = ex::just(42) | test_adaptor{};
using normal_state = ex::connect_result_t<decltype(sender), normal_receiver>;
using inlinable_state_t = ex::connect_result_t<decltype(sender), inlinable_receiver>;
STATIC_REQUIRE(ex::operation_state<normal_state>);
STATIC_REQUIRE(ex::operation_state<inlinable_state_t>);

auto nested_sender = sender | test_adaptor{};
using nested_normal_state = ex::connect_result_t<decltype(nested_sender), normal_receiver>;
using nested_inlinable_state = ex::connect_result_t<decltype(nested_sender), inlinable_receiver>;
STATIC_REQUIRE(sizeof(normal_state) == sizeof(nested_normal_state));
STATIC_REQUIRE(sizeof(inlinable_state_t) == sizeof(nested_inlinable_state));

auto [result] = ex::sync_wait(nested_sender).value();
REQUIRE(result == 42);

auto nested_4_sender = ex::just(67) | test_adaptor{} | test_adaptor{} | test_adaptor{}
| test_adaptor{};
using nested_4_normal_state = ex::connect_result_t<decltype(nested_4_sender), normal_receiver>;
using nested_4_inlinable_state =
ex::connect_result_t<decltype(nested_4_sender), inlinable_receiver>;
STATIC_REQUIRE(sizeof(normal_state) == sizeof(nested_4_normal_state));
STATIC_REQUIRE(sizeof(inlinable_state_t) == sizeof(nested_4_inlinable_state));

auto [result1] = ex::sync_wait(nested_4_sender).value();
REQUIRE(result1 == 67);
}
Loading