Try   HackMD

Coroutines TS Customization Points

This is intended to be a reference/cheatsheet/decoder ring, not an introduction. If you don't know anything about the Coroutine TS, I suggest reading Lewis Baker's excellent series of blog posts. Or if you are feeling brave, just go read the current working draft of the TS.

This is also intended for people who need to know the inner workings of the TS, such as people writing libraries. This is not for people who just want to use coroutines with one of these libraries, since that is much simpler.

Table of customization points

A few long names are split up so that the table formats better. All names should be considered a single possibly-qualified identifier. Everything is explained in more detail below. These are split into sections based on what the customization point is attached to.

Name Required Attached To Notes
get_return_object()
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Promise What gets returned from the coroutine (eg lazy<T> or generator<T>)
get_return_object_on _allocation_failure() Promise (static) If present, std::nothrow will be passed to operator new
return_value() or return_void()
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Promise Argument is what co_return takes
unhandled_exception()
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Promise
yield_value() with co_yield Promise
await_transform() Promise
initial_suspend()
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Promise
final_suspend()
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Promise
Constructor() can be defaulted Promise Either nullary (default ctor) or gets to "preview" coro parameters
operator new() Promise or global Also gets "parameter preview"
operator delete() Promise or global No "parameter preview", but supports sized-deallocation




std:: coroutine_traits<T, Args...>::promise_type Only provide this if you can't add a nested promise_type on your coroutine type. Also gets "parameter preview" for types.




operator co_await() ProtoAwaitable or ADL free-function Converts a ProtoAwaitable to an Awaitable




await_ready()
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Awaitable
await_suspend()
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Awaitable Only called if await_ready() returns false. Bool return has opposite meaning as await_ready() (ie. true means suspend)
await_resume()
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Awaitable

Table of std:: things

Name Meaning
coroutine_traits<T>::promise_type T::promise_type unless specialized
coroutine_handle<PromiseType> A magical type that lets you resume or destroy a suspended coroutine. Can also get to or from a PromiseType&
coroutine_handle<> Type-erased version of above. The unerased type implicitly converts to the erased type. This conversion is harmless and encouraged when you don't need access to a Promise
noop_coroutine_handle A coroutine_handle that when returned from await_suspend() behaves as-if void or true were returned
noop_coroutine() Returns a noop_coroutine_handle
suspend_always A trivial Awaitable that always suspends and yields void. Useful for returning from initial_suspend(), final_suspend(), and yield_value()
suspend_never A trivial Awaitable that never suspends and yields void. Useful for returning from initial_suspend() and final_suspend()

coroutine_handle<PromiseType>

template <class Promise = void>
struct coroutine_handle;

/**
 * The type-erased base type for all coroutine_handles.
 * Allows libraries that don't care about the Promise type to deal
 * with a single concrete type which represents all coroutines.
 */
template <>
struct coroutine_handle<void> // Usually written as coroutine_handle<>
{
    /// Make a null coroutine_handle.
    constexpr coroutine_handle() noexcept;
    constexpr coroutine_handle(nullptr_t) noexcept;
    coroutine_handle& operator=(nullptr_t) noexcept;

    /// Is this a null coroutine_handle?
    constexpr explicit operator bool() const noexcept;

    /// Convert to/from an opaque address. Useful with C APIs.
    constexpr void* address() const noexcept;
    constexpr static coroutine_handle from_address(void* addr);  
    
    //
    // Remaining methods can only be called while the coro is suspended!
    //

    /// Is the coro suspended at the `final_suspend()` point?
    /// WARNING: this should never be used for coroutines that may return
    /// `suspend_never` from `final_suspend()`, since it will either return
    /// true or dereference a pointer to deleted memory!
    bool done() const;
    
    /// These both resume the coroutine where is last suspended.
    /// coroutine_handle objects can be treated as a `void()` function
    /// object, which simplfies integration into existing libraries
    /// which take the equivalent of a `std::function<void()>`.
    void resume() const;
    void operator()() const { resume(); }
    
    /// Destroy any local variables currently in scope inside the coroutine
    /// and delete the allocated frame buffer. This will automatically be
    /// called if resumed following the call to `final_suspend()`, or if
    /// that doesn't actually suspend. May be called any time the coroutine
    /// is supsended, not just at `final_suspend()`, however not all coroutines
    /// will be prepared to not wake up after suspending part way through, so
    /// This should only be done in cases where it is known to be safe.
    void destroy() const;
};

/**
 * The specific type used by the coroutine machinery.
 */
template <class Promise>
struct coroutine_handle
{
    /// Can be constructed/retrieved the same as above.
    constexpr coroutine_handle() noexcept;
    constexpr coroutine_handle(nullptr_t) noexcept;
    coroutine_handle& operator=(nullptr_t) noexcept;
    
    constexpr void* address() const noexcept;
    constexpr static coroutine_handle from_address(void* addr);
        
    /// Also supports conversion to/from a reference to the Promise.
    /// This is the only API gained by not type erasing.
    Promise& promise() const;
    static coroutine_handle from_promise(Promise&);
  
    /// Implicit conversion to erase Promise type
    constexpr operator coroutine_handle<>() const noexcept;
  
    /// Also provides the API from coroutine_handle<void>
    constexpr explicit operator bool() const noexcept;
    bool done() const;
    void operator()() const;
    void resume() const;
    void destroy() const;
};

/// coroutine_handles are ordered by `addres()`. Maybe we'll get a <=> as well?
constexpr bool operator==(coroutine_handle<> x, coroutine_handle<> y) noexcept;
constexpr bool operator!=(coroutine_handle<> x, coroutine_handle<> y) noexcept;
constexpr bool operator<(coroutine_handle<> x, coroutine_handle<> y) noexcept;
constexpr bool operator>(coroutine_handle<> x, coroutine_handle<> y) noexcept;
constexpr bool operator<=(coroutine_handle<> x, coroutine_handle<> y) noexcept;
constexpr bool operator>=(coroutine_handle<> x, coroutine_handle<> y) noexcept;
template <class Promise> struct hash<experimental::coroutine_handle<Promise>>;

Coroutine function transformation

A coroutine function like MyCoroType func(Args... args) { BODY; } is transformed to something roughly like the following. This is done for any function where BODY contains any of co_return, co_await, or co_yield (which is really also just a co_await).

The detailed expansions below may refer back to local variables declared here. The names are not actually visible to the user-provided BODY statements.

// If `func` is a method on some Type, pretend it is just a free funtion
// with a `Type&` argument before the declared arguments.
MyCoroType func(Args... args) {
    using PromiseType = std::coroutine_traits<MyCoroType, Args...>::promise_type;
    
    // All local variables are actually constructed inside this buffer.
    // The arguments to `func()` will also be copied into this buffer.
    // The allocation may be elided if the compiler can prove it safe.
    // `operator new` will first be looked up in PromiseType, then globally.
    // If the found name is not callable with `args...`, it will try again
    // without passing them. This is known as "parameter preview" and allows
    // things like passing the allocator that should be used.
    void* coro_frame;
    size_t coro_frame_size = /*...*/;
    if constexpr (PromiseType::get_return_object_on_allocation_failure exists) {
        coro_frame = operator new(coro_frame_size,
                                  std::nothrow,
                                  args...);
        if (!coro_frame) {
            return PromiseType::get_return_object_on_allocation_failure();
        }
    } else {
        coro_frame = operator new(coro_frame_size,
                                  args...);
    }
    
    
    // Will fall back to `PromiseType()` if it can't
    // be constructed like this. Note that the promise object
    // will be allocated in the coro_frame buffer and will live
    // as long as the coroutine. This makes a handy place to
    // stash coordination state that would normally go in
    // some refcounted SharedState object referenced from both
    // the promise and MyCoroType objects. If you do this,
    // make sure that `final_suspend()` actually suspends!
    auto promise = PromiseType(args...);
    auto my_handle = std::coroutine_handle<PromiseType>::from_promise(promise);
        
    auto protoReturnValue = promise.get_return_object();
    
    // This is the value that is actually returned to the caller
    // of `func()`. It is unspecified whether this conversion
    // happens before calling `initial_suspend()` or the first
    // time the coroutine actually suspends or returns, which
    // is when `func()` returns to its caller. If it is called
    // late, you can use that to optimize for cases where the
    // coro completes without ever suspending.
    MyCoroType actualReturnValue = std::move(protoReturnValue);
    
    // This may move inside the try block in a future version of the TS.
    co_await promise.initial_suspend();
    try {
        // This will contain usually a call to `promise.return_void()`
        // or `promise.return_value()`. See co_return below.
        BODY;
        
        if constexpr (promise has return_void()) {
            // Make rolling off the end of BODY well defined for
            // coros that (logically) return `void`. For other
            // kinds ofcoros this is UB that your compiler should
            // warn about, just like in normal functions.
            co_return;
        }
    } catch (...) {
        // This can get the exception using either `throw;`
        // or `std::current_exception()`. If this throws,
        // the coroutine is considered suspended at its final
        // suspend point, without calling final_suspend().
        promise.unhandled_exception();
    }
    
    co_await promise.final_suspend();
    // It is common for coroutines to never be resumed from
    // this call if they suspend here. In that case, the
    // following line will never execute, and something
    // else must free the storage by calling `destory()`
    // on the coroutine handle.
    
    // Will fallback to single-argument form.
    operator delete(coro_frame, coro_frame_size);
}

co_await

The expression co_await makeProtoAwaitable() expands to roughly the following. Commented out portions are optional and will be used if present. SUSPEND_HERE() is shorthand for returning from the call to the function that creates the coroutine object, or from the call to resume() on the handle. SUSPEND_HERE_AND_TAIL_JUMP_TO(dest) is shorthand for doing SUSPEND_HERE() and then after poping the stack frame calling dest.resume() without actually returning control to the caller/resumer.

[&]() -> decltype(auto) { // Immediately invoked below
    // Note: The await_trasnform operation is only done on
    // users' co_awaits, not the implicit co_awaits on the
    // returns from yield_value, initial_suspend, and
    // final_suspend, because the promise is already involved
    // in those.
    auto&& Awaitable awaitable =
        /*operator co_await*/ (
            /*promise.await_transform*/ (
                makeProtoAwaitable()
            )
        );
    if (!awaitable.await_ready()) {
        // If await_ready() returns false, the coroutine enters a "suspended" state.
        // This means that it is legal to do things like call destroy() or
        // resume() on the handle passed to await_suspend(). However, rather than
        // calling resume(), it is better to return the handle so that a tail-call
        // is made rather than recursively growing the stack.
        using SuspendRet = decltype(awaitable.await_suspend(my_handle));
        if constexpr (is_void<SuspendRet>()) {
            awaitable.await_suspend(my_handle)
            SUSPEND_HERE();
        } else if constexpr (is_bool<SuspendRet>()) {
            if (awaitable.await_suspend(my_handle)) {
                SUSPEND_HERE();
            }
        } else {
            auto dest = awaitable.await_suspend(my_handle);
            if (dest is a noop_coroutine_handle) {
                SUSPEND_HERE();
            } else {
                // This is effectively a no-op if dest == my_handle
                SUSPEND_HERE_AND_TAIL_JUMP_TO(dest);
            }
        }
        
        // Once we get here, either by being resumed, or never reaching a SUSPEND,
        // the coroutine leaves the "suspended" state, and it is no longer legal
        // to call resume() or destroy() on the handle.
    }
    
    return awaitable.await_resume(); // May return void
}();

Yes operator co_await() is optional, and yes that does mean that often co_await expressions will not actually call any operator co_await.

co_yield

The expression co_yield value expands to co_await promise.yield_value(value) (see above for how to expand the co_await). Promises will usually just return suspend_always() here, however that isn't required. If it returns a non-void ProtoAwaitable, you have a two-way generator that supports things like auto input = co_yield output;

While similar to co_await, there is one notable grammatical difference due to their different intended usages. co_await binds tightly to its argument like any other unary operator, such as *expr. co_yield binds loosely like return expr. So co_await a + b is parsed as (co_await a) + b, while co_yield a + b is parsed as co_yield (a + b). In most cases you don't need to worry about this since they behave how you would expect, but be careful with complex expressions if you are using either in a "weird" way.

co_return

The statement co_return; is rewritten to promise.return_void(); and co_return expr; is rewritten to promise.return_value(expr);. In both cases, exection will then leave the BODY; of the transformed coroutine, and go to the call to promise.final_suspend().

This is logically like normal return except in a coroutine. Normal return is banned in a coroutine, and co_return can't be in a non-coroutine function because it will turn it into a coroutine.

Templates

These are some templates (not in the c++ sense) ready for copy-pasting! They don't need to be a templates (in the c++ sense), but often are, or are inner types of templates.

Nota Bene: These have not been tested. Treat them as slideware rather than as production code.

Promise

template <typename T>
struct promise_type {
    // If you care about parameter preview
    template <typename... Args>
    promise_type(Args&&...);
    
    auto initial_suspend() { return std::experimental::suspend_always(); }
    auto final_suspend() { return std::experimental::suspend_always(); }
    auto get_return_object() { return MyCoroType(this); }
    void return_value(T val) {}
    void unhandled_exception() {}
    
    // If you need to see ProtoAwaitables before `operator co_await`
    // is called on them.
    template <typename T>
    auto await_transform(T&& awaitable);
        
    // If you want to control allocation
    template <typename... Args>
    void* operator new(size_t size, Args...);
    void operator delete(void* p, size_t size);
    
    // Only if you care about recovering from allocation failure
    static MyCoroType get_return_object_on_allocation_failure();
};

struct promise_type /*void*/ {
    // If you care about parameter preview
    template <typename... Args>
    promise_type(Args&&...);
    
    auto initial_suspend() { return std::experimental::suspend_always(); }
    auto final_suspend() { return std::experimental::suspend_always(); }
    auto get_return_object() { return MyCoroType(this); }
    void return_void() {}
    void unhandled_exception() {}
    
    // If you want to support co_yield.
    // You rarely have this in the same type as return_value.
    void yield_value(T) {}
        
    // If you need to see ProtoAwaitables before `operator co_await`
    // is called on them.
    template <typename T>
    auto await_transform(T&& awaitable);
    
    // If you want to control allocation
    template <typename... Args>
    void* operator new(size_t size, Args...);
    void operator delete(void* p, size_t size);
    
    // Only if you care about recovering from allocation failure
    static MyCoroType get_return_object_on_allocation_failure();
};

Awaitable

template <typename T>
struct MyAwaitable {
    bool await_ready() { return false; }
    void await_suspend(coroutine_handle<> h) {}
    T await_resume() {}
};

Future-like type

template <typename T>
struct MyFuture {
    struct promise_type; // see above
    
    // Can also just *be* an Awaitable with no `operator co_await`
    MyAwaitable<T> operator co_await(); // see above
};

Generator (Range) type

These use co_yield. While this returns T& from the iterator's operator*() and stores a copy of the value, a real implementation should strongly consider returning T&& and storing a pointer, as is done by std::generator. See P2529 by the same author as this page for more info.

template <typename T>
struct generator {
    struct promise_type {
        suspend_never initial_suspend() { return {}; }
        suspend_always final_suspend() { return {}; }

        auto yield_value(T v) {
             val.emplace(std::move(v));
             return suspend_always();
        }

        auto get_return_object() { return generator(this); }

        void return_void() {}
        void unhandled_exception() { throw; }

        auto coro() { return coroutine_handle<promise_type>::from_promise(*this); }

        std::optional<T> val;
    };

    generator(generator&& source) : p(std::exchange(source.p, nullptr)) {}
    explicit generator(promise_type* p) :p(p) {}
    ~generator() {
        if (p) p->coro().destroy();
    }

    // generator<T> is a Range. You can use it in a range-for loop.
    struct EndSentinel{};
    auto end() { return EndSentinel(); }
    auto begin() {
        struct Iter {
            // You probably want to add iterator_tag and friends.
            // That isn't needed if you only want to support range-for.
            bool operator!=(EndSentinel) const { return !p->coro().done(); }
            void operator++() { p->coro().resume(); }
            T& operator*() const { return *p->val; }

            promise_type* p;
        };
        return Iter{p};
    }

    promise_type* p;
};