Tobias Jacob
Tobias Jacob

Reputation: 51

C++20 coroutines: Why is the promise type seperated from the coroutine object?

A typical coroutine object looks like this:

struct Coroutine
{
    struct promise_type
    {
        int value;
        Coroutine get_return_object() {return this; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_value(int v) {
            value = v;
        }
        void unhandled_exception() {};
    };

    Coroutine(promise_type *p) : handle(std::coroutine_handle<promise_type>::from_promise(*p)) {
    };

    ~Coroutine() {
        handle.destroy();
    }

    std::coroutine_handle<promise_type> handle;
};

Why is the promise_type separated from the coroutine. Does it make sense to have multiple coroutines for a single promise, or multiple promises for a single coroutine? Is there a use case for that?

Because otherwise, if only one promise can exist per coroutine, wouldn't it have been enough to embed the return value into the Coroutine structure, e. g.:

struct Coroutine
{
   int value;
   std::suspend_never initial_suspend() { return {}; }
   std::suspend_never final_suspend() noexcept { return {}; }
   void return_value(int v) {
       value = v;
   }
   void unhandled_exception() {};
};

Upvotes: 1

Views: 1465

Answers (1)

Nicol Bolas
Nicol Bolas

Reputation: 473946

The return value of a coroutine is distinct from the promise type for several reasons.

  1. That a function is a coroutine is intended to be an implementation detail of the function. Nothing in a function declaration requires it to be a coroutine. Including the return type. This means that a function that returns a coroutine future type may or may not be a coroutine. This allows for things like passing a coroutine through you; you call some function that itself schedules its resumption, and you just return its return value as is.

  2. Separating the two is intended to allow existing types that support continuations to be re-implemented internally as coroutines. Or more generally, to allow a type to represent any kind of asynchronous process without caring about whether its a coroutine or not. For example, if std::future<T> permitted .then continuations, one could easily allow you to co_await on them by just having the co_await machinery use .then to schedule the coroutine's resumption. But you could also just call .then directly with a regular function.

    If the return value object was the coroutine, it would be impossible (without dynamic polymorphism of some form) to make such a change internally without an external change to the interface.

Also, you seem to be mistaking the return value of a function that happens to be a coroutine with the coroutine itself. This is not the case. The coroutine is the actual call stack allocated by the coroutine machinery, which includes the promise object. The return value is linked to the coroutine, but it isn't the coroutine itself. The coroutine and its promise object are (usually) heap-allocated and stored elsewhere, with the return value merely referencing it.

Upvotes: 4

Related Questions