Skip to content

add a submit customization point for connecting and immediately starting an operation #332

@ericniebler

Description

@ericniebler
Collaborator

@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 just for it we may not be able to change let_value to use it because it would change the ABI of operation states. :-/

Activity

ericniebler

ericniebler commented on Mar 28, 2025

@ericniebler
CollaboratorAuthor

additionally, we could permit submit to return void as a way of saying that nothing needs to be kept alive for the duration of the operation.

ericniebler

ericniebler commented on Apr 7, 2025

@ericniebler
CollaboratorAuthor

I've discovered a potential problem with this idea. The default implementation of submit shown above returns an object that connects and starts an operation in its constructor. If start 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:

start_detached( let_value( ..., [] { return then(just(), []{}); } ) );

let_value wants to call submit on the then sender returned from the lambda and emplace the result into its operation state. emplace-ing the object returned from submit causes the then operation to be started, which completes synchronously.

since the let_value sender was started by start_detached, when the operation completes, it will delete the let_value operation state. that happens synchronously while emplace-ing the result of submit into let_value's operation state.

in practice, the object returned from submit (which is being emplaced into the let_value operation state), will call start 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 as let_value ensures that nothing touches its op state once submit 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 use connect and start directly.

[*]: in NVIDIA/stdexec#1519, that required extreme care in the __variant::emplace_from, which let_value uses to store the result of submit, since the variant gets destroyed before __variant::emplace_from returns.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2enhancementNew feature or requestneeds-paperNeeds a paper to be written

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @ericniebler

        Issue actions

          add a `submit` customization point for connecting and immediately starting an operation · Issue #332 · cplusplus/sender-receiver