Skip to content

Improvements to receiver interface (explicit datum types, generic set_result) #264

Open
@lewissbaker

Description

@lewissbaker

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 to set_value().
  • More efficient forwarding of small, trivial datum values
    Most algorithms at the moment implement set_value() taking a pack of forwarding references to the datums. This means that when passing trivial types, like int, 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 the set_value(), set_stopped(), set_error() member functions of the receiver. It would simplify some of this code to instead have a single set_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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions