Open
Description
There are a few things that I think we could improve with the receiver interface. In particular I would like to investigate improving:
- The ability to disambiguate between prvalue and xvalue result-types
It can be ambiguous what the value-category of a datum is intended to be when deducing this only from the argument types passed toset_value()
. - More efficient forwarding of small, trivial datum values
Most algorithms at the moment implementset_value()
taking a pack of forwarding references to the datums. This means that when passing trivial types, likeint
, as a datum, it needs to be materialized on the stack, a pointer to this passed as an argument, and then the pointer is dereferenced when consumed. If we could somehow know that the datum was a trivially-copyable type and then compute the parameter type to pass by value instead of by reference then it could help with more efficient code-gen. - Simpler forwarding/generic handling of results
For a lot of algorithms that need to store results of an operation, or forward them through, we typically need to write the same code in each of theset_value()
,set_stopped()
,set_error()
member functions of the receiver. It would simplify some of this code to instead have a singleset_result()
method that could handle all of the signals generically.
The change that I think could help to address all of these issues is to define the following:
namespace std::execution {
struct value_tag {};
struct error_tag {};
struct stopped_tag {};
template<typename Tag, typename... Datums>
struct result_t;
template<typename... Vs>
struct result_t<value_tag, Vs...> {};
template<typename E>
struct result_t<error_tag, E> {};
template<>
struct result_t<stopped_tag> {};
template<typename... Vs>
using value_t = result_t<value_tag, Vs...>;
template<typename E>
using error_t = result_t<error_tag, E>;
using stopped_t = result_t<stopped_tag>;
template<typename... Vs>
inline constexpr value_t<Vs...> value{};
template<typename E>
inline constexpr error_t<E> error{};
inline constexpr stopped_t stopped{};
// Sigs must be result_t specializations
template<typename... Sigs>
struct completion_signatures {
static constexpr std::size_t size = sizeof...(Sigs);
};
template<typename T>
inline constexpr bool enable_pass_by_value =
std::is_trivially_copy_constructible_v<T> &&
std::is_trivially_move_constructible_v<T> &&
std::is_trivially_destructible_v<T> &&
(sizeof(T) <= (2 * sizeof(void*)));
template<typename T>
using parameter_type = std::conditional_t<enable_pass_by_value<T>, T, T&&>;
template<typename T>
parameter_type<T> forward_parameter(T& x) noexcept {
static_assert(std::is_nothrow_constructible_v<parameter_type<T>, T>);
return static_cast<T&&>(x);
}
struct set_result_t {
template<typename Receiver, typename Tag, typename... Datums>
static void operator()(result_t<Tag, Datums...>, parameter_type<Datums>... datums) noexcept {
using sig_t = result_t<Tag, Datums...>;
static_assert(noexcept(r.set_result(sig_t{}, forward_parameter<Datums>(datums)...)));
r.set_result(sig_t{}, forward_parameter<Datums>(datums)...);
}
};
inline constexpr set_result_t set_result{};
template<typename... Vs>
struct set_value_t {
template<typename Receiver>
static void operator()(Receiver&& r, parameter_type<Vs>... vs) noexcept {
using sig_t = value_t<Vs...>;
static_assert(noexcept(r.set_result(sig_t{}, forward_parameter<Vs>(vs)...)));
r.set_result(sig_t{}, forward_parameter<Vs>(vs)...);
}
};
template<typename... Vs>
inline constexpr set_value_t<Vs...> set_value{};
template<typename E>
struct set_error_t {
template<typename Receiver>
static void operator()(Receiver&& r, parameter_type<E> e) noexcept {
using sig_t = error_t<E>;
static_assert(noexcept(r.set_result(sig_t{}, forward_parameter<E>(e))));
r.set_result(sig_t{}, forward_parameter<E>(e));
}
};
template<typename E>
inline constexpr set_error_t<E> set_error{};
struct set_stopped_t {
template<typename Receiver>
static void operator()(Receiver&& r) noexcept {
static_assert(noexcept(r.set_result(stopped_t{})));
r.set_result(stopped_t{});
}
};
inline constexpr set_stopped_t set_stopped{};
} // namespace std::execution
Then, invoking a completion method on a receiver can be done as follows:
some_receiver r;
std::execution::set_value<int, std::string>(r, 42, "hello"s);
std::execution::set_error<std::error_code>(r, std::make_error_code(std::errc::not_supported));
std::execution::set_stopped(r);
And for the forwarding use-cases:
struct some_receiver {
op_state* op;
template<typename Tag, typename... Datums>
void set_result(std::execution::result_t<Tag, Datums...> tag, std::execution::parameter_type<Datums>... datums) noexcept {
std::execution::set_result(op->receiver,
tag, std::execution::forward_parameter<Datums>(datums)...);
}
// ...
};
Or for overloading value vs error/stopped differently:
struct some_receiver {
op_state* op;
template<typename... Vs>
void set_result(std::execution::value_t<Vs...>, std::execution::parameter_type<Vs>... vs) noexcept {
// handle values...
}
template<typename Tag, typename... Datums>
requires one_of<Tag, std::execution::stopped_tag, std::execution::error_tag>
void set_result(std::execution::result_t<Tag, Datums...> tag, std::execution::parameter_type<Datums>... datums) noexcept {
// Forward error/stopped
std::execution::set_result(op->receiver, tag, std::execution::forward_parameter<Datums>(datums)...);
}
// ...
};
This design would address the issues mentioned above:
- The explicit listing of the Datum types as deducible template parameters of the result_t tag enables disambiguating which completion signature is being invoked.
- Listing the Datum types explicitly means that we can then compute an appropriate parameter type for the
set_result()
function - passing trivial types by-value and non-trivial types by rvalue-reference. - Passing the
result_t
tag type as the first parameter means we can generically implement handlers for all of the signals with a single function template, at least for cases where the functions otherwise have the same behaviour.