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 *)⊤
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;
}
longjmp
andthrow
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