2
\$\begingroup\$

This is my attempt to implement the style of coroutines I described in my answer to Small coroutine class. What do you think of it?

(My unusual dialect is due to my compiling with -std=c++1z on a moderately recent Xcode. It doesn't support std::apply or if constexpr or std::is_same_v, but it does support fold-expressions and std::invoke.)

I'm particularly interested to learn of corner cases that aren't being handled correctly in terms of the metaprogramming. E.g. I tried to handle void correctly, and to disallow reference types, and so on, but if I missed something, or if something can be done simpler, let me know!

#include <cassert>
#include <csetjmp>
#include <functional>
#include <tuple>
#include <utility>
#include <vector>

#ifndef STD_APPLY_IS_ALREADY_PROVIDED
// Copied straight from http://en.cppreference.com/w/cpp/utility/apply
namespace std {
namespace detail {
template <class F, class Tuple, std::size_t... I>
constexpr decltype(auto) apply_impl(F &&f, Tuple &&t, std::index_sequence<I...>)
{
    return std::invoke(std::forward<F>(f), std::get<I>(std::forward<Tuple>(t))...);
}
}  // namespace detail

template <class F, class Tuple>
constexpr decltype(auto) apply(F &&f, Tuple &&t)
{
    return detail::apply_impl(
        std::forward<F>(f), std::forward<Tuple>(t),
        std::make_index_sequence<std::tuple_size<std::decay_t<Tuple>>::value>{});
}
} // namespace std
#endif // STD_APPLY_IS_ALREADY_PROVIDED

struct coroutine_done {};

struct coroutine_layout {
    std::vector<char> stack_;
    jmp_buf on_yield_;
    jmp_buf on_resume_;
};

template<typename Output, typename... Inputs>
struct coroutine_base : coroutine_layout
{
    Output get_output()
    {
        try {
            throw;
        } catch (Output arg_to_yield) {
            return arg_to_yield;
        }
    }

    std::tuple<Inputs...> yield(Output r)
    {
        if (setjmp(on_resume_)) {
            try {
                throw;
            } catch (std::tuple<Inputs...> args_to_resume) {
                return args_to_resume;
            }
        } else {
            try {
                throw std::move(r);
            } catch (...) {
                longjmp(on_yield_, 1);
            }
        }
    }
};

template<typename... Inputs>
struct coroutine_base<void, Inputs...> : coroutine_layout
{
    void get_output() {}

    std::tuple<Inputs...> yield()
    {
        if (setjmp(on_resume_)) {
            try {
                throw;
            } catch (std::tuple<Inputs...> args_to_resume) {
                return args_to_resume;
            }
        } else {
            longjmp(on_yield_, 1);
        }
    }
};

template<typename F> class coroutine;

template<typename Output, typename... Inputs>
class coroutine<Output(Inputs...)> : private coroutine_base<Output, Inputs...>
{
    static_assert((true && ... && !std::is_reference<Inputs>::value), "Reference parameters to c.resume() are not allowed");
    static_assert(!std::is_reference<Output>::value, "Reference parameters to c.yield() are not allowed");
public:
    using done = coroutine_done;

    coroutine(const coroutine&) = delete;
    coroutine& operator=(const coroutine&) = delete;
    coroutine(coroutine&&) = default;
    coroutine& operator=(coroutine&&) = default;

    coroutine(void (*f)(coroutine&, Inputs...))
    {
        this->stack_.resize(4096);
        // Exploit a non-standard VLA and undefined behavior to adjust the stack pointer
        // until it points at the buffer we just heap-allocated.
        char * volatile top = (char *)&top;
        volatile char bump[(intptr_t)top - (intptr_t)&this->stack_.back()];  // rsp <= (rsp - (rsp - &stack.back())) <== &stack.back()
        assert(&this->stack_.front() <= &bump[0] && &bump[0] <= &this->stack_.back());
        // Get a copy of `f` onto the new, heap-allocated, stack.
        do_more_(f);
    }

    void do_more_(void (*f)(coroutine&, Inputs...))
    {
        assert(&this->stack_.front() <= (char*)&f && (char*)&f <= &this->stack_.back());
        if (setjmp(this->on_resume_)) {
            try {
                throw;
            } catch (std::tuple<Inputs...> args_to_resume) {
                auto f_this = [f, this](auto&&... as) { f(*this, std::forward<decltype(as)>(as)...); };
                std::apply(f_this, std::move(args_to_resume));
            }
            throw coroutine::done{};
        }
    }

    template<typename... Argx>
    Output resume(Argx... args)
    {
        static_assert(sizeof...(Argx) == sizeof...(Inputs), "Wrong number of arguments to resume()");
        if (setjmp(this->on_yield_)) {
            return this->get_output();
        } else {
            try {
                throw std::tuple<Inputs...>(std::forward<Argx>(args)...);
            } catch(...) {
                longjmp(this->on_resume_, 1);
            }
        }
    }

    using coroutine_base<Output, Inputs...>::yield;
};

// TEST HARNESS FOLLOWS

#include <iostream>

void ff(coroutine<void(void)>& c)
{
    for (int i=0; i < 10; ++i) {
        printf("ff with i=%d\n", i);
        c.yield();
    }
}

void gg(coroutine<void(void)>& c)
{
    for (int i=0; i < 10; ++i) {
        printf("gg with i=%d\n", i);
        c.yield();
    }
}

void ff(coroutine<int(int)>& c, int x)
{
    for (int i=0; i < 10; ++i) {
        printf("ff with i=%d x=%d\n", i, x);
        std::tie(x) = c.yield(0);
    }
}

void gg(coroutine<int(int, int)>& c, int x, int y)
{
    for (int i=0; i < 10; ++i) {
        printf("gg with i=%d x=%d y=%d\n", i, x, y);
        std::tie(x,y) = c.yield(x+y);
    }
}

int main()
{
    coroutine<int(int)> c2(ff);
    coroutine<int(int,int)> c(gg);
    int i = 4;
    std::cout << c.resume(1,2);
    std::cout << c.resume(3,4);
    c2.resume(i);
    auto d2 = std::move(c2);
    d2.resume(i);
    std::cout << c.resume(9,10);
    std::cout << c.resume(11,12);
    d2.resume(i);
    d2.resume(i);
    return 0;
}
\$\endgroup\$
  • \$\begingroup\$ boost already provides co-routine class. Using longjmp and throw is not really a good way to implement this feature as the stack state is not preserved but boost provides a low level library that you can use that may help: Boost context \$\endgroup\$ – Martin York Dec 30 '16 at 18:09

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.