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;
}
G. Sliepen
68.7k3 gold badges74 silver badges179 bronze badges
asked Dec 30, 2016 at 2:28
\$\endgroup\$
1
  • \$\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\$ Commented Dec 30, 2016 at 18:09

0

Know someone who can answer? Share a link to this question via email, Twitter, or Facebook.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.