Skip to content

[coroutine.handle] clarify whether it's legal to create handles for promise base classes #6039

@jacobsa

Description

@jacobsa

Here is a hypothetical coroutine promise type, which has a base class that is responsible for storing a handle to the coroutine that should be resumed when the promise's coroutine finishes:

struct PromiseBase {
  // The handle to resume when done, also used for walking the linked list as
  // described below.
  std::coroutine_handle<PromiseBase> waiter;
};

template <typename Result>
struct Promise : PromiseBase {
  // Obtain a handle for this promise's coroutine, in the form required when
  // initializing PromiseBase::waiter. For example, this can be called when
  // co_awaiting a call to another coroutine with promise type Promise<T> for
  // some type T, at which point we have to fill in our own handle as the
  // waiter for the callee.
  std::coroutine_handle<PromiseBase> get_base_handle() {
    std::coroutine_handle<PromiseBase>::from_promise(*this);
  }
  
  // ... other promise machinery ...
};

The reason I need to do this, splitting the base class out and storing a handle with the base class as the promise type instead of using std::coroutine_handle<>, is to be able to walk the chain of waiters as a linked list. If Foo0 calls and waits on Foo1 … calls and waits on FooN, I need to be able to walk the chain of coroutines from FooN back to Foo0 (after synchronizing on them being suspended). This is useful for example when cleaning up after cancellation while suspended, or for implementing user-friendly async stack traces. I can't do this using std::coroutine_handle<Promise<T>>, because T may vary between the different functions.


My general question here is whether walking such a list is intended to be legal. I see the lack of clarity on this as a shortcoming in the standard's wording (or maybe I'm just missing something obvious). The code I've got above is the closest I can come up with to a UB-free way to implement it:

  • According to [coroutine.handle.con], the precondition for calling from_promise is only that the parameter be a reference to the promise object of a coroutine. That is certainly the case for the expression *this in Promise::get_base_handle, and presumably also the case even when that reference is expressed with a different type.

  • I see no mention anywhere in [coroutine.handle] of a requirement that the T in std::coroutine_handle<T> must be the promise type of a coroutine, rather than a base class of a promise type.

But that said, I can't see how std::coroutine_handle<T>::from_promise would be efficiently implementable without requiring something additional about T. For example if the actual promise type inherits from multiple classes including T, then the address of the promise is not necessarily the address of the T object within the promise. Therefore std::coroutine_handle<T>::from_promise can't statically know the offset of the T object within the coroutine frame if its input is of type T& (rather than being templated on the promise type).


To be clear, I would like this pattern to be legal in some (potentially restricted) form. I can't see how to implement things like good async stack traces without it. But I'm looking for clarification on whether it's intended to be legal, and perhaps the restrictions under which it is legal. Two ideas come to mind:

  1. std::coroutine_handle<T>::from_promise could be templated on the input reference type, with the restriction that T must be a base class of the referred-to type. I believe this would allow the implementation to calculate the correct offset for the coroutine frame, even in the face of multiple inheritance.

  2. The signature of std::coroutine_handle<T>::from_promise could be left the same, but the precondition tightened to require that the parameter must reference the promise of a coroutine where the promise has the same address as addressof(parameter). I'm not 100% sure this is guaranteed by the standard, but I believe this means the example above is legal because there is no multiple inheritance to cause their addresses to differ.

Apologies if I've got any facts incorrect here; mostly I'm looking for clarification from the experts.

(See also here for some previous discussion on Stack Overflow.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    not-editorialIssue is not deemed editorial; the editorial issue is kept open for tracking.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions