-
Notifications
You must be signed in to change notification settings - Fork 5
Description
@haggaie makes an interesting observation about the following sender:
let_value(whatever, [](things...) { return just(stuff...); })
let_value
is going to call the lambda with whatever
's value completions, and then immediately connect
and start
the resulting sender (just(stuff...)
). the just
sender's operation state, including copies of stuff...
, will be stored in let_value
's operation state. but after the just
operation is started, stuff...
isn't needed anymore and is just uselessly taking up space in let_value
's operation state.
imagine we add back a submit
customization that connects and immediately starts an operation ... but that returns the operation (instead of returning void
like the old one did). it has an obvious default implementation in terms of connect
and start
:
template <class Sndr, class Rcvr>
auto submit(Sndr&& sndr, Rcvr rcvr)
{
struct storage
{
op(Sndr&& sndr, Rcvr rcvr)
: op_(connect(forward<Sndr>(sndr), move(rcvr)))
{
start(op_);
}
connect_result_t<Sndr, Rcvr> op_;
};
return storage{forward<Sndr>(sndr), move(rcvr)};
}
but a sender like just
could customize it like:
struct _empty {};
template <class... Values>
struct just_sender
{
template <class Self, class Rcvr>
_empty submit(this Self&& self, Rcvr rcvr)
{
std::apply(
[&](auto... vals) noexcept { set_value(move(rcvr), move(vals)...); },
forward<Self>(self).vals_
);
return {};
}
...
tuple<Values...> vals_;
};
in this case, the returned object doesn't even need to store the receiver.
adding this customization point after c++26 would be possible as an extension, but we may not be able to customize we may not be able to change just
for itlet_value
to use it because it would change the ABI of operation states. :-/
Activity
ericniebler commentedon Mar 28, 2025
additionally, we could permit
submit
to returnvoid
as a way of saying that nothing needs to be kept alive for the duration of the operation.submit
that combinesconnect
andstart
NVIDIA/stdexec#1519ericniebler commentedon Apr 7, 2025
I've discovered a potential problem with this idea. The default implementation of
submit
shown above returns an object thatconnect
s andstart
s an operation in its constructor. Ifstart
completes synchronously, it could cause the destruction of*this
, which hasn't yet been fully constructed. I'm pretty sure that's technically UB.Here is a situation where that happens:
let_value
wants to callsubmit
on thethen
sender returned from the lambda andemplace
the result into its operation state.emplace
-ing the object returned fromsubmit
causes thethen
operation to be started, which completes synchronously.since the
let_value
sender was started bystart_detached
, when the operation completes, it willdelete
thelet_value
operation state. that happens synchronously whileemplace
-ing the result ofsubmit
intolet_value
's operation state.in practice, the object returned from
submit
(which is being emplaced into thelet_value
operation state), will callstart
as the very last thing in its constructor. that object has only one member (then
's operation state), which has already been fully constructed. so long aslet_value
ensures that nothing touches its op state oncesubmit
has been called, no harm happens in practice.[*] i have implemented this, and the various sanitizers let it slide.but if we want to standardize this, we have a problem. we can't standardize UB.
EDIT: if we had a sender attribute to tell us whether a sender could potentially complete inline, we could use that to decide whether to call
submit
or to useconnect
andstart
directly.[*]: in NVIDIA/stdexec#1519, that required extreme care in the
__variant::emplace_from
, whichlet_value
uses to store the result ofsubmit
, since the variant gets destroyed before__variant::emplace_from
returns.