Doc. no:  [TBD]
Audience: LEWG
Date:     2018-07-12
Reply-To: Eric Fiselier (eric@efcs.ca)
          Vinnie Falco (vinnie.falco@gmail.com)

A Clang/LVVM Implementor's Perspective on Coroutines

Contents

Abstract

Stackless coroutines provide the ability to pause and resume a function's execution at well defined suspend points, where the storage requirements are known at compile time. Years of experience has proven that assistance from the compiler is necessary to transform concisely annotated C++ code into a stackless coroutine with a desirable run-time performance profile. This paper describes the experience of an implementor adding support for the Coroutines TS to Clang/LLVM, then compares and constrasts that with the experience of adding support for Resumable Expressions to the same version of Clang/LLVM.

Introduction

A coroutine is a thread of execution containing well-defined suspend points from which the flow of control may be paused and resumed. A stackful coroutine preserves the stack of the thread of execution starting from the entry point to the suspend point. A stackless coroutine preserves a constant amount of state known at compile time. Stackful coroutines are uncontroversial and relatively straightforward; many performant implementations have existed as library-only solutions for decades. This paper discusses stackless coroutine implementations exclusively; henceforth they are referred to simply as "coroutines".

All coroutine implementations share a common algorithm, called the coroutine transformation. This algorithm takes ordinary code annotated with suspend points and inserts additional code allowing the thread of execution to be paused at each suspend point and later resumed. At a minimum, the transformation must store sufficient state to identify the last suspend point from which the coroutine should be resumed. An implementation may also choose to preserve the values of some or all local variables and function arguments.

A typical implementation must contain at least the following features, otherwise the traditional functionality of a coroutine cannot be realized:

We use the term coroutine to also refer to the object produced by the coroutine transformation in a program, and the term coroutine abstraction (or just "abstraction") to refer to the interface offered by a particular implementation for creating and interacting with the coroutine. While an abstraction may include additional features such as the ability to yield values, these features are not required. An abstraction containing only the features above is termed a minimal abstraction.

Coroutines TS

As of this writing, the leading proposal for introducing coroutines into the C++ language is the Coroutines TS described in n4723[3]. The proposal introduces several new keywords including co_await, co_yield, and co_resume. We abbreviate the name of Coroutines TS by referring it to just "TS" or "co-await." This section highlights some of the notable parts of the TS, by starting with an example program written against it:

#include "generator.hpp"
#include <experimental/coroutine>
#include <cstdio>

generator<const char>
hello(const char* p)
{
    while (*p) {
        co_yield *p++;
    }
}

int main()
{
    for (const auto c : hello("Hello, world")) {
        std::putchar(c);
    }
}

After compilation using the latest Visual C++ compiler and the command line switch "/await" to enable co-await features, the executable produces the following output when launched:

Hello, world

The appearance of the co_yield keyword identifies the function hello as a coroutine. Use of the co_yield keyword allows the coroutine to suspend and deliver an intermediate result to the caller. Since the type of the expression *p++ is char const&, it might not be immediately clear how this value interacts with the return type of the function to give the loop in main a value. The TS prescribes a rich set of rules for converting the yielded value into executable code. In this case, the compiler looks to the return type of the function and checks for the nested type promise_type. If found, a local variable p of the promise type is added to the generated coroutine code. The emitted code for the expression co_yield e then becomes p.yield_value(e). The generator class template is an example of coroutine middleware. The full header file is provided in Listing 1.

Resumable Expressions

Analysis

References

[1] [N4723] Coroutines TS

[2] [P0114R0] Resumable Expressions (revision 1)

Acknowledgements 1

Listing 1

///////////////////////////////////////////////////////////////////////////////
// Copyright (c) Lewis Baker
// Licenced under MIT license. See LICENSE.txt for details.
///////////////////////////////////////////////////////////////////////////////

/*
Original file located at
https://github.com/lewissbaker/cppcoro/blob/2492c0782911cdf7fe803642040efff3faa3c28a/include/cppcoro/generator.hpp

This is a derivative work
*/

#ifndef CPPCORO_GENERATOR_HPP_INCLUDED
#define CPPCORO_GENERATOR_HPP_INCLUDED

#include <experimental/coroutine>
#include <type_traits>
#include <utility>

template<typename T>
class generator;

namespace detail {

template<typename T>
class generator_promise
{
    pointer_type m_value;

public:
    using value_type = std::remove_reference_t<T>;
    using reference_type = std::conditional_t<std::is_reference_v<T>, T, T&>;
    using pointer_type = value_type*;

    generator_promise() = default;

    generator<T> get_return_object() noexcept;

    constexpr std::experimental::suspend_always initial_suspend() const { return {}; }
    constexpr std::experimental::suspend_always final_suspend() const { return {}; }

    template<
        typename U,
        typename = std::enable_if_t<std::is_same<U, T>::value>>
    std::experimental::suspend_always yield_value(U& value) noexcept
    {
        m_value = std::addressof(value);
        return {};
    }

    std::experimental::suspend_always yield_value(T&& value) noexcept
    {
        m_value = std::addressof(value);
        return {};
    }

    void unhandled_exception()
    {
        std::rethrow_exception(std::current_exception());
    }

    void return_void()
    {
    }

    reference_type value() const noexcept
    {
        return *m_value;
    }

    // Don't allow any use of 'co_await' inside the generator coroutine.
    template<typename U>
    std::experimental::suspend_never await_transform(U&& value) = delete;
};

template<typename T>
class generator_iterator
{
    using coroutine_handle = std::experimental::coroutine_handle<generator_promise<T>>;
    coroutine_handle m_coroutine;

public:
    using iterator_category = std::input_iterator_tag;
    using difference_type = std::size_t;
    using value_type = std::remove_reference_t<T>;
    using reference = value_type&;
    using pointer = value_type*;

    explicit generator_iterator(std::nullptr_t) noexcept
        : m_coroutine(nullptr)
    {}

    explicit generator_iterator(coroutine_handle coroutine) noexcept
        : m_coroutine(coroutine)
    {}

    bool operator==(const generator_iterator& other) const noexcept
    {
        return m_coroutine == other.m_coroutine;
    }

    bool operator!=(const generator_iterator& other) const noexcept
    {
        return !(*this == other);
    }

    generator_iterator& operator++()
    {
        m_coroutine.resume();
        if (m_coroutine.done())
        {
            m_coroutine = nullptr;
        }

        return *this;
    }

    // Don't support post-increment as that would require taking a
    // copy of the old value into the returned iterator as there
    // are no guarantees it's still going to be valid after the
    // increment is executed.
    generator_iterator operator++(int) = delete;

    reference operator*() const noexcept
    {
        return m_coroutine.promise().value();
    }

    pointer operator->() const noexcept
    {
        return std::addressof(operator*());
    }
};

} // detail

template<typename T>
class generator
{
public:

    using promise_type = detail::generator_promise<T>;
    using iterator = detail::generator_iterator<T>;

    generator() noexcept
        : m_coroutine(nullptr)
    {}

    generator(generator&& other) noexcept
        : m_coroutine(other.m_coroutine)
    {
        other.m_coroutine = nullptr;
    }

    generator(const generator& other) = delete;

    ~generator()
    {
        if (m_coroutine)
        {
            m_coroutine.destroy();
        }
    }

    generator& operator=(generator other) noexcept
    {
        swap(other);
        return *this;
    }

    iterator begin()
    {
        if (m_coroutine)
        {
            m_coroutine.resume();
            if (!m_coroutine.done())
            {
                return iterator{ m_coroutine };
            }
        }

        return iterator{ nullptr };
    }

    iterator end() noexcept
    {
        return iterator{ nullptr };
    }

    void swap(generator& other) noexcept
    {
        std::swap(m_coroutine, other.m_coroutine);
    }

private:

    friend class detail::generator_promise<T>;

    explicit generator(std::experimental::coroutine_handle<promise_type> coroutine) noexcept
        : m_coroutine(coroutine)
    {}

    std::experimental::coroutine_handle<promise_type> m_coroutine;

};

template<typename T>
void swap(generator<T>& a, generator<T>& b)
{
    a.swap(b);
}

namespace detail {

template<typename T>
generator<T> generator_promise<T>::get_return_object() noexcept
{
    using coroutine_handle = std::experimental::coroutine_handle<generator_promise<T>>;
    return generator<T>{ coroutine_handle::from_promise(*this) };
}

} // detail

#endif