3
\$\begingroup\$

I've been playing around with the coroutines implementation in gcc 10.2.0 (Debian build) based on the sparse documentation at https://en.cppreference.com/w/cpp/language/coroutines . The target, for now, was to see how to use the functionality to enable creation of Python-style generator functions.

So here's what I have so far. I've tested valgrind cleanness of the code; and in addition to the included test cases demonstrating cancelling a pending generator coroutine, I've also tested the paths of normal exit of the coroutine and of unhandled exception propagating to the caller.

Note that with current gcc, you still need to add the -fcoroutines flag to compile this.

Main goal here is to get feedback on the parts specific to the coroutines functionality (and not so much the probably incomplete iterator implementation, where I just put in enough to satisfy the ranges library requirements).

(And yes, in projects using this code, as opposed to this quick experiment, I'd probably split the generic parts up into separate headers.)

#include <coroutine>
#include <exception>
#include <iostream>
#include <ranges>
#include <tuple>
#include <utility>

// Class inspired by unique_ptr, whose main purpose is to have a destructor
// which calls destroy on the contained handle.  Note that in order for this
// to work, there should be a suspend point in the promise type's
// final_suspend() hook; otherwise, on completion of the coroutine, this
// will be left with a dangling handle.
template <typename promise_type>
class unique_coroutine_handle {
private:
    std::coroutine_handle<promise_type> m_handle;

public:
    explicit unique_coroutine_handle(promise_type& promise)
    {
        m_handle = std::coroutine_handle<promise_type>::from_promise(promise);
    }
    explicit unique_coroutine_handle(std::coroutine_handle<promise_type> i_handle)
        : m_handle(std::move(i_handle))
    {
    }
    unique_coroutine_handle(const unique_coroutine_handle&) = delete;
    unique_coroutine_handle(unique_coroutine_handle&& other)
        : m_handle(std::move(other.m_handle))
    {
        other.m_handle = nullptr;
    }
    unique_coroutine_handle& operator=(const unique_coroutine_handle&) = delete;
    unique_coroutine_handle& operator=(unique_coroutine_handle&& other)
    {
        if (m_handle)
            m_handle.destroy();
        m_handle = std::move(other.m_handle);
        other.m_handle = nullptr;
    }

    ~unique_coroutine_handle()
    {
        if (m_handle)
            m_handle.destroy();
    }

    const std::coroutine_handle<promise_type>& get() const { return m_handle; }
};

template <typename T>
class generator {
public:
    class promise_type {
    public:
        std::suspend_never initial_suspend() { return {}; };

        // Note that in addition to the requirement imposed by the need not
        // to leave the generator with a dangling unique_coroutine_handle,
        // the suspend here is also important to ensure the promise object
        // is not destroyed before iteration on the generator gets a chance
        // to check for res==nullptr to see the terminating condition.
        std::suspend_always final_suspend() { return {}; }

        std::suspend_always yield_value(const T& val)
        {
            res = &val;
            return {};
        }
        void return_void() {
            res = nullptr;
        }
        void unhandled_exception()
        {
            eptr = std::current_exception();
            res = nullptr;
        }
        generator get_return_object()
        {
            return { *this };
        }

    private:
        const T* res;
        std::exception_ptr eptr;

        friend class ::generator<T>;
        friend class ::generator<T>::iterator;
    };

private:
    promise_type& promise;
    unique_coroutine_handle<promise_type> handle;

    generator(promise_type& i_promise)
        : promise { i_promise }
        , handle { i_promise }
    {
    }

public:
    generator() = default;
    generator(generator&&) = default;
    ~generator() = default;

    struct sentinel {
    };
    class iterator {
    private:
        generator* obj = nullptr;
        iterator(generator* i_obj)
            : obj(i_obj)
        {
        }

    public:
        iterator() = default;
        iterator(const iterator&) = default;
        iterator(iterator&&) = default;
        iterator& operator=(const iterator&) = default;
        iterator& operator=(iterator&&) = default;
        ~iterator() = default;

        using value_type = const T;
        using reference = const T&;
        using difference_type = std::ptrdiff_t;
        iterator& operator++()
        {
            obj->handle.get().resume();
            if (obj->promise.eptr)
                std::rethrow_exception(obj->promise.eptr);
            return *this;
        }
        class postinc_result_proxy {
        private:
            T val_copy;

        public:
            const T& operator*() const { return val_copy; }

            friend class ::generator<T>::iterator;
        };
        postinc_result_proxy operator++(int)
        {
            T val_copy = *(obj->promise.res);
            ++(*this);
            return { std::move(val_copy) };
        }
        reference operator*() const {
            return *(obj->promise.res);
        }
        bool operator==(sentinel) const {
            return obj->promise.res == nullptr;
        }
        bool operator!=(sentinel) const {
            return obj->promise.res != nullptr;
        }

        friend class ::generator<T>;
    };
    using const_iterator = iterator;

    iterator begin() {
        if (promise.eptr)
            std::rethrow_exception(promise.eptr);
        return { this };
    }
    sentinel end() { return {}; }
    const_iterator begin() const {
        if (promise.eptr)
            std::rethrow_exception(promise.eptr);
        return { this };
    }
    sentinel end() const { return {}; }
    const_iterator cbegin() const {
        if (promise.eptr)
            std::rethrow_exception(promise.eptr);
        return { this };
    }
    sentinel cend() const { return {}; }
};



// example usage below
struct verbose {
    verbose() { std::cout << "verbose constructor called\n"; }
    ~verbose() { std::cout << "verbose destructor called\n"; }
};

generator<int> fibs()
{
    verbose destruct_check;
    int a = 0, b = 1;
    while (true) {
        co_yield b;
        std::tie(a, b) = std::make_tuple(b, a + b);
    }
}

generator<int> odd_fibs()
{
    for (int n : fibs())
        if (n % 2 != 0)
            co_yield n;
}

int main()
{
    {
        auto f = fibs();
        for (int n : f | std::views::take(10))
            std::cout << n << '\n';
    }

    {
        auto of = odd_fibs();
        for (int n : of | std::views::take_while([](int n) { return n <= 500; }))
            std::cout << n << '\n';
    }

    return 0;
}
\$\endgroup\$
  • \$\begingroup\$ Do you know about the CppCoro library? If you want to reimplement this yourself, then adding the reinvent-the-wheel tag to the question would be appropriate. \$\endgroup\$ – G. Sliepen Oct 20 '20 at 18:39
  • 1
    \$\begingroup\$ No, I didn't know about it. (Though given the comments on the cppreference page I linked, I was well aware that this was very likely one of the intended use cases of the feature.) \$\endgroup\$ – Daniel Schepler Oct 20 '20 at 18:45
  • \$\begingroup\$ I also reimplemented-the-wheel at github.com/Quuxplusone/coro — e.g. coro.godbolt.org/z/fx897q — if you want to compare and contrast. I'm unlikely to write a real answer here though, sorry. \$\endgroup\$ – Quuxplusone Oct 21 '20 at 4:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.