Title
task's coroutine frame may be released late
Status
new
Section
[exec.task]
Submitter
Dietmar Kühl

Created on 2025-08-31.00:00:00 last changed 2 weeks ago

Messages

Date: 2025-09-02.20:31:35

Proposed resolution:

In [task.state], add exposition-only data members result and error to the exposition-only class state:

namespace std::execution {
  template<class T, class Environment>
  template<receiver Rcvr>
  class task<T, Environment>::state {           // exposition only
  ...
  Environment               environment;   // exposition only
  optional<T>               result;        // exposition only; present only if is_void_v<T> is false
  exception_ptr             error;         // exposition-only
};
}

Remove the exposition-only data members result and errors from the class promise_type in [task.promise]:

namespace std::execution {
  template<class T, class Environment>
  class task<T, Environment>::promise_type {
  ...
  stop_token_type   token;  // exposition only
  optional<T>       result; // exposition only; present only if is_void_v<T> is false
  error-variant     errors; // exposition only
};
}

The definition of error-variant isn't needed, i.e., remove [task.promise] paragraph 2:

-2- error-variant is a variant<monostate, remove_cvref_t<E>...>, with duplicate types removed, where E... are template arguments of the specialization of execution::completion_signatures denoted by error_types.

In [task.promise] change paragraph 7 to use the members added to state:

auto final_suspend() noexcept

-7- Returns: An awaitable object of unspecified type ([expr.await]) whose member functions arrange for the completion of the asynchronous operation associated with STATE(*this) by invoking. Let st be a reference to STATE(*this). The asynchronous completion first destroys the coroutine frame using st.handle.destroy() and then invokes:

  • -7.1- set_error(std::move(RCVR(*this)st.rcvr), std::move(est.error)) if errors.index() is greater than zero and e is the value held by errorsbool(st.error) is true, otherwise
  • -7.2- set_value(std::move(RCVR(*this)st.rcvr)) if is_void<T> is true, and otherwise
  • -7.3- set_value(std::move(RCVR(*this)st.rcvr), *st.result).

Change the specification of yield_value to destroy the coroutine frame before invoking the set_error completion, i.e., change [task.promise] paragraph 9:

-9- Returns: An awaitable object of unspecified type ([expr.await]) whose member functions arrange for the calling coroutine to be suspended and then completes the asynchronous operation associated with STATE(*this) by. Let st be a reference to STATE(*this). Then the asynchronous operation completes by first destroying the coroutine frame using st.handle.destroy() and then invoking set_error(std::move(RCVR(*this)st.rcvr), Cerr(std::move(err.error))).

Change the specification of unhandled_stopped to destroy the coroutine frame before invoking the set_stopped completion, i.e., change [task.promise] paragraph 13:

coroutine_handle<> unhandled_stopped();

-13- Effects: Completes the asynchronous operation associated with STATE(*this) by. Let st be a reference to STATE(*this). The asynchronous operation is completed by first destroying the coroutine frame using st.handle.destroy() and then invoking set_stopped(std::move(RCVR(*this)st.rcvr)).

Date: 2025-08-31.00:00:00

The specification of task doesn't spell out when the coroutine frame is destroyed (i.e., when handle.destroy() is called). As a result the lifetime of arguments passed to the coroutine and/or of local variables in the coroutine body may be longer than expected.

The intention is that the coroutine frame is destroyed before any of the completion functions is called. One implication of this requirement is that the result and error objects can't be stored in the promise_type when the completion function is called although the exposition-only members result and errors imply exactly that. Instead the data needs to be stored in the operation state object or it needs to be moved to a different place before calling destroy().

The proposed resolution is to add a paragraph to the specification of promise_type in [task.promise] that spells out that the coroutine frame is destroyed before any of the completion functions is called. To actually do that the exposition-only members result and errors can't remain as members of the promise_type. While writing the relevant change it turned out that errors is a variant which only ever stores an exception_ptr (the other potential errors are immediately reported via the awaiter return from yield_value). Thus, the variant can be replaced with an exception_ptr.

History
Date User Action Args
2025-09-02 20:31:35adminsetmessages: + msg15013
2025-08-31 00:00:00admincreate