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
217 changes: 112 additions & 105 deletions include/stdexec/__detail/__let.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
#include "__variant.hpp"

#include <exception>
#include <type_traits>
#include <utility>

namespace stdexec {
//////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -282,32 +284,90 @@ namespace stdexec {

//! The core of the operation state for `let_*`.
//! This gets bundled up into a larger operation state (`__detail::__op_state<...>`).
template <class _Receiver, class _Fun, class _SetTag, class _Env2, class... _Tuples>
template <class _SetTag, class _Sender, class _Fun, class _Receiver, class... _Tuples>
struct __let_state {
using __fun_t = _Fun;
using __env2_t = _Env2;
using __env_t = __join_env_t<_Env2, env_of_t<_Receiver>>;
using __rcvr_t = __receiver_with_env_t<_Receiver, _Env2>;
using __env2_t = __let::__env2_t<_SetTag, env_of_t<const _Sender&>, env_of_t<const _Receiver&>>;
using __second_rcvr_t = __receiver_with_env_t<_Receiver, __env2_t>;
struct __first_rcvr_t {
using receiver_concept = ::stdexec::receiver_t;
__let_state& __state;
_Receiver& __rcvr;
template <typename _Tag, typename... _Args>
constexpr void __impl(_Tag __tag, _Args&&... __args) noexcept {
if constexpr (std::is_same_v<_SetTag, _Tag>) {
constexpr bool __nothrow_store = (__nothrow_decay_copyable<_Args> && ...);
constexpr bool __nothrow_invoke = __nothrow_callable<_Fun, __decay_t<_Args>&...>;
using __sender_t = __call_result_t<_Fun, __decay_t<_Args>&...>;
using __submit_t = __submit_result<__sender_t, __env2_t, _Receiver>;
constexpr bool __nothrow_submit = noexcept(
__state.__storage_.template emplace<__submit_t>(
std::declval<__sender_t>(), std::declval<__second_rcvr_t>()));
constexpr bool __nothrow = __nothrow_store && __nothrow_invoke && __nothrow_submit;
const auto __impl = [&]() noexcept(__nothrow) {
auto& __tuple =
__state.__args_.emplace_from(__tup::__mktuple, static_cast<_Args&&>(__args)...);
auto&& __sender = __tuple.apply(static_cast<_Fun&&>(__state.__fun_), __tuple);
__state.__storage_.template emplace<__monostate>();
__second_rcvr_t __r{__rcvr, static_cast<__env2_t&&>(__state.__env2_)};
auto& __op = __state.__storage_.template emplace<__submit_t>(
static_cast<__sender_t&&>(__sender), static_cast<__second_rcvr_t&&>(__r));
__op.submit(static_cast<__sender_t&&>(__sender), static_cast<__second_rcvr_t&&>(__r));
};
if constexpr (__nothrow) {
__impl();
} else {
STDEXEC_TRY {
__impl();
}
STDEXEC_CATCH_ALL {
::stdexec::set_error(static_cast<_Receiver&&>(__rcvr), std::current_exception());
}
}
Comment on lines +306 to +325
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i learned from lewis that it is simpler to use try unconditionally, and only guard the set_error call in the catch block. the optimized codegen is identical.

Suggested change
const auto __impl = [&]() noexcept(__nothrow) {
auto& __tuple =
__state.__args_.emplace_from(__tup::__mktuple, static_cast<_Args&&>(__args)...);
auto&& __sender = __tuple.apply(static_cast<_Fun&&>(__state.__fun_), __tuple);
__state.__storage_.template emplace<__monostate>();
__second_rcvr_t __r{__rcvr, static_cast<__env2_t&&>(__state.__env2_)};
auto& __op = __state.__storage_.template emplace<__submit_t>(
static_cast<__sender_t&&>(__sender), static_cast<__second_rcvr_t&&>(__r));
__op.submit(static_cast<__sender_t&&>(__sender), static_cast<__second_rcvr_t&&>(__r));
};
if constexpr (__nothrow) {
__impl();
} else {
STDEXEC_TRY {
__impl();
}
STDEXEC_CATCH_ALL {
::stdexec::set_error(static_cast<_Receiver&&>(__rcvr), std::current_exception());
}
}
STDEXEC_TRY {
auto& __tuple =
__state.__args_.emplace_from(__tup::__mktuple, static_cast<_Args&&>(__args)...);
auto&& __sender = __tuple.apply(static_cast<_Fun&&>(__state.__fun_), __tuple);
__state.__storage_.template emplace<__monostate>();
__second_rcvr_t __r{__rcvr, static_cast<__env2_t&&>(__state.__env2_)};
auto& __op = __state.__storage_.template emplace<__submit_t>(
static_cast<__sender_t&&>(__sender), static_cast<__second_rcvr_t&&>(__r));
__op.submit(static_cast<__sender_t&&>(__sender), static_cast<__second_rcvr_t&&>(__r));
}
STDEXEC_CATCH_ALL {
if constexpr (!__nothrow) {
::stdexec::set_error(static_cast<_Receiver&&>(__rcvr), std::current_exception());
}
}

} else {
__tag(static_cast<_Receiver&&>(__rcvr), static_cast<_Args&&>(__args)...);
}
}
template <typename... _Args>
constexpr void set_value(_Args&&... __args) noexcept {
__impl(::stdexec::set_value, static_cast<_Args&&>(__args)...);
}
template <typename... _Args>
constexpr void set_error(_Args&&... __args) noexcept {
__impl(::stdexec::set_error, static_cast<_Args&&>(__args)...);
}
template <typename... _Args>
constexpr void set_stopped(_Args&&... __args) noexcept {
__impl(::stdexec::set_stopped, static_cast<_Args&&>(__args)...);
}
constexpr decltype(auto) get_env() const noexcept {
return ::stdexec::get_env(__rcvr);
}
};

using __result_variant = __variant_for<__monostate, _Tuples...>;
using __submit_variant = __variant_for<
using __op_state_variant = __variant_for<
__monostate,
__mapply<__submit_datum_for<_Receiver, _Fun, _SetTag, _Env2>, _Tuples>...
>;

template <class _ResultSender, class _OpState>
auto __get_result_receiver(const _ResultSender&, _OpState& __op_state) -> decltype(auto) {
return __rcvr_t{__op_state.__rcvr_, __env2_};
::stdexec::connect_result_t<_Sender, __first_rcvr_t>,
__mapply<__submit_datum_for<_Receiver, _Fun, _SetTag, __env2_t>, _Tuples>...>;

constexpr explicit __let_state(_Sender&& __sender, _Fun __fun, _Receiver& __r) noexcept(
__nothrow_connectable<_Sender, __first_rcvr_t>
&& std::is_nothrow_move_constructible_v<_Fun>)
: __fun_(static_cast<_Fun&&>(__fun))
, __env2_(
__let::__mk_env2<_SetTag>(::stdexec::get_env(__sender), ::stdexec::get_env(__r))) {
__storage_.emplace_from(
::stdexec::connect, static_cast<_Sender&&>(__sender), __first_rcvr_t{*this, __r});
}

STDEXEC_IMMOVABLE_NO_UNIQUE_ADDRESS
_Fun __fun_;
STDEXEC_IMMOVABLE_NO_UNIQUE_ADDRESS
_Env2 __env2_;
__env2_t __env2_;
//! Variant to hold the results passed from upstream before passing them to the function:
__result_variant __args_{};
//! Variant type for holding the operation state from connecting
//! the function result to the downstream receiver:
__submit_variant __storage_{};
//! Variant type for holding the operation state of the currently in flight operation
__op_state_variant __storage_{};
};

// The set_value completions of:
Expand Down Expand Up @@ -504,6 +564,17 @@ namespace stdexec {
}
};

template <class _Sender, class _Fun>
struct __data_t {
_Sender __sndr;
_Fun __fun;
};

template <typename _Sender>
using __sender_of = decltype((std::declval<__data_of<_Sender>>().__sndr));
template <typename _Sender>
using __fun_of = decltype((std::declval<__data_of<_Sender>>().__fun));
Comment on lines +573 to +576
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

microbenchmarks showed that this somewhat bizarre implementation of declval compiles about 2x faster than the standard one:

  template <class _Tp, bool _Noexcept = true>
  using __declfn_t = auto (*)() noexcept(_Noexcept) -> _Tp;

  template <class _Tp>
  extern __declfn_t<_Tp &&> __declval;
Suggested change
template <typename _Sender>
using __sender_of = decltype((std::declval<__data_of<_Sender>>().__sndr));
template <typename _Sender>
using __fun_of = decltype((std::declval<__data_of<_Sender>>().__fun));
template <typename _Sender>
using __sender_of = decltype((__declval<__data_of<_Sender>>().__sndr));
template <typename _Sender>
using __fun_of = decltype((__declval<__data_of<_Sender>>().__fun));


//! Implementation of the `let_*_t` types, where `_SetTag` is, e.g., `set_value_t` for `let_value`.
template <class _SetTag>
struct __let_t {
Expand All @@ -512,7 +583,7 @@ namespace stdexec {
template <sender _Sender, __movable_value _Fun>
auto operator()(_Sender&& __sndr, _Fun __fun) const -> __well_formed_sender auto {
return __make_sexpr<__let_t<_SetTag>>(
static_cast<_Fun&&>(__fun), static_cast<_Sender&&>(__sndr));
__data_t{static_cast<_Sender&&>(__sndr), static_cast<_Fun&&>(__fun)});
Comment on lines 513 to +586
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a problem. the structured binding of let_value is part of its interface. this should compile:

auto [tag, fn_copy, sndr_copy] = stdexec::let_value(sndr, fn);

you can use transform_sender to swivel the fn and the sndr into a __data_t object. exec::repeat_n does this, so you can crib from that.

}

template <class _Fun>
Expand All @@ -524,114 +595,50 @@ namespace stdexec {

template <class _SetTag>
struct __let_impl : __sexpr_defaults {
static constexpr auto get_attrs = []<class _Fun, class _Child>(
const _Fun&,
[[maybe_unused]]
const _Child& __child) noexcept {
// BUGBUG:
return stdexec::get_env(__child);
//return __attrs<__let_t<_SetTag>, _Child, _Fun>{};
};
static constexpr auto get_attrs =
[]<class _Child, class _Fun>(const __data_t<_Child, _Fun>& __data) noexcept {
// BUGBUG:
return stdexec::get_env(__data.__sndr);
//return __attrs<__let_t<_SetTag>, _Child, _Fun>{};
};

static constexpr auto get_completion_signatures =
[]<class _Self, class _Env>(_Self&&, _Env&&...) noexcept {
static_assert(sender_expr_for<_Self, __let_t<_SetTag>>);
if constexpr (__decay_copyable<_Self>) {
using __result_t =
__completions_t<__let_t<_SetTag>, __data_of<_Self>, __child_of<_Self>, _Env>;
using __result_t = __completions_t<
__let_t<_SetTag>,
std::remove_cvref_t<__fun_of<_Self>>,
__sender_of<_Self>,
_Env>;
return __result_t{};
} else {
return __mexception<_SENDER_TYPE_IS_NOT_COPYABLE_, _WITH_SENDER_<_Self>>{};
}
};

static constexpr auto get_state =
[]<class _Receiver, __decay_copyable _Sender>(_Sender&& __sndr, const _Receiver& __rcvr)
requires sender_in<__child_of<_Sender>, env_of_t<_Receiver>>
[]<class _Receiver, __decay_copyable _Sender>(_Sender&& __sndr, _Receiver& __rcvr)
requires sender_in<__sender_of<_Sender>, env_of_t<_Receiver>>
{
static_assert(sender_expr_for<_Sender, __let_t<_SetTag>>);
using _Fun = __decay_t<__data_of<_Sender>>;
using _Child = __child_of<_Sender>;
using _Env2 = __env2_t<_SetTag, env_of_t<const _Child&>, env_of_t<const _Receiver&>>;
using __mk_let_state = __mbind_front_q<__let_state, _Receiver, _Fun, _SetTag, _Env2>;

using _Child = __sender_of<_Sender>;
using _Fun = __decay_t<__fun_of<_Sender>>;
using __mk_let_state = __mbind_front_q<__let_state, _SetTag, _Child, _Fun, _Receiver>;
using __let_state_t = __gather_completions_of<
_SetTag,
_Child,
env_of_t<_Receiver>,
__q<__decayed_tuple>,
__mk_let_state
>;

return __sndr.apply(
static_cast<_Sender&&>(__sndr),
[&]<class _Fn, class _Child>(__ignore, _Fn&& __fn, _Child&& __child) {
// TODO(ericniebler): this needs a fallback
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please don't loose this comment. i need to come back to this.

_Env2 __env2 =
__let::__mk_env2<_SetTag>(stdexec::get_env(__child), stdexec::get_env(__rcvr));
return __let_state_t{static_cast<_Fn&&>(__fn), static_cast<_Env2&&>(__env2)};
});
__mk_let_state>;
auto&& [__tag, __data] = static_cast<_Sender&&>(__sndr);
return __let_state_t(
__forward_like<_Sender>(__data).__sndr, __forward_like<_Sender>(__data).__fun, __rcvr);
};

//! Helper function to actually invoke the function to produce `let_*`'s sender,
//! connect it to the downstream receiver, and start it. This is the heart of
//! `let_*`.
template <class _State, class _OpState, class... _As>
static void __bind_(_State& __state, _OpState& __op_state, _As&&... __as) {
// Store the passed-in (received) args:
auto& __args = __state.__args_.emplace_from(__tup::__mktuple, static_cast<_As&&>(__as)...);
// Apply the function to the args to get the sender:
auto __sndr2 = __args.apply(std::move(__state.__fun_), __args);
// Create a receiver based on the state, the computed sender, and the operation state:
auto __rcvr2 = __state.__get_result_receiver(__sndr2, __op_state);
// Connect the sender to the receiver and start it:
using __result_t = decltype(submit_result{std::move(__sndr2), std::move(__rcvr2)});
auto& __op = __state.__storage_
.template emplace<__result_t>(std::move(__sndr2), std::move(__rcvr2));
__op.submit(std::move(__sndr2), std::move(__rcvr2));
}

template <class _OpState, class... _As>
static void __bind(_OpState& __op_state, _As&&... __as) noexcept {
using _State = decltype(__op_state.__state_);
using _Receiver = decltype(__op_state.__rcvr_);
using _Fun = _State::__fun_t;
using _Env2 = _State::__env2_t;
using _JoinEnv2 = __join_env_t<_Env2, env_of_t<_Receiver>>;
using _ResultSender = __mcall<__result_sender_fn<_SetTag, _Fun, _JoinEnv2>, _As...>;

_State& __state = __op_state.__state_;
_Receiver& __rcvr = __op_state.__rcvr_;

if constexpr (
(__nothrow_decay_copyable<_As> && ...) && __nothrow_callable<_Fun, __decay_t<_As>&...>
&& __nothrow_connectable<_ResultSender, __result_receiver_t<_Receiver, _Env2>>) {
__bind_(__state, __op_state, static_cast<_As&&>(__as)...);
} else {
STDEXEC_TRY {
__bind_(__state, __op_state, static_cast<_As&&>(__as)...);
}
STDEXEC_CATCH_ALL {
using _Receiver = decltype(__op_state.__rcvr_);
stdexec::set_error(static_cast<_Receiver&&>(__rcvr), std::current_exception());
}
}
}

static constexpr auto complete = []<class _OpState, class _Tag, class... _As>(
__ignore,
_OpState& __op_state,
_Tag,
_As&&... __as) noexcept -> void {
if constexpr (__same_as<_Tag, _SetTag>) {
// Intercept the channel of interest to compute the sender and connect it:
__bind(__op_state, static_cast<_As&&>(__as)...);
} else {
// Forward the other channels downstream:
using _Receiver = decltype(__op_state.__rcvr_);
_Tag()(static_cast<_Receiver&&>(__op_state.__rcvr_), static_cast<_As&&>(__as)...);
}
};
static constexpr auto start =
[]<typename _State, typename _Receiver>(_State& __state, _Receiver&) noexcept {
::stdexec::start(__state.__storage_.template get<1>());
};
};
} // namespace __let

Expand Down
48 changes: 48 additions & 0 deletions test/stdexec/algos/adaptors/test_let_value.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -396,4 +396,52 @@ namespace {
ex::start(op);
CHECK(*ptr == 5);
}

TEST_CASE(
"let_value destroys the first operation state before invoking the sender factory",
"[adaptors][let_value]") {
const auto ptr = std::make_shared<int>(5);
CHECK(ptr.use_count() == 1);
auto first = ex::just() | ex::then([ptr = ptr]() { });
CHECK(ptr.use_count() == 2);
auto sender = ex::let_value(std::move(first), [&]() {
CHECK(ptr.use_count() == 2);
return ex::just();
});
CHECK(ptr.use_count() == 2);
auto op = ex::connect(std::move(sender), expect_void_receiver{});
CHECK(ptr.use_count() == 2);
ex::start(op);
CHECK(ptr.use_count() == 1);
}

struct immovable_sender {
using sender_concept = ::stdexec::sender_t;
template <typename... Args>
consteval auto get_completion_signatures(const Args&...) const & noexcept {
return ::stdexec::completion_signatures_of_t<decltype(::stdexec::just()), Args...>{};
}
template <typename Receiver>
auto connect(Receiver r) const & noexcept {
return ::stdexec::connect(::stdexec::just(), std::move(r));
}
immovable_sender() = default;
immovable_sender(const immovable_sender&) {
throw std::logic_error("Unexpected copy");
}
};
static_assert(::stdexec::sender<immovable_sender>);
static_assert(::stdexec::sender<const immovable_sender&>);
static_assert(::stdexec::sender_in<immovable_sender, ::stdexec::env<>>);
static_assert(::stdexec::sender_in<const immovable_sender&, ::stdexec::env<>>);

TEST_CASE(
"If the sender factory returns a reference to a sender that reference is passed to connect",
"[adaptors][let_value]") {
const immovable_sender s;
auto just = ex::just();
auto sender = ex::let_value(just, [&]() -> decltype(auto) { return (s); });
auto op = ex::connect(sender, expect_void_receiver{});
ex::start(op);
}
} // namespace
Loading