This project is the natural extension to my attempt to make a templatedgenerator coroutine. This time, I tried what I called a "task" (may be not the real term for this) : it generates a value or not, and is recursively awaitable. It can call other coroutines which each pause will also pause the main coroutine. It was really difficult to implement recursion, and I wonder if there is a better option, like one bundled with coroutine support directly. My first attempt was to make a variable innerCoroutine but I think it suffers from performances issues because when there is, like 100 coroutines on the stack, each call will recursively try to found the most inner coroutine, and with this method each resume() call is 0(1). I tested with unit cases, so there was many trials until all tests passes so it may be a bit complicated. I wonder if is it okay, especially from the point of view of undefined behavior and memory leaks.
When I started learning C++ coroutines, I found them overly unnecessary complicated, but I think I begin to figure out they are not that complicated, and highly powerful and customizable, at least I hope.
#include <coroutine>
#include <optional>
#include <cassert>
#include <memory>
#include <vector>
#include <iostream>
struct promise_type_vars
{
using stack = std::vector<std::coroutine_handle<>>;
std::shared_ptr<stack> stack_;
};
template<typename self>
struct promise_type_base : promise_type_vars
{
using promise_type = typename self::promise_type;
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
[[noreturn]] void unhandled_exception() { throw; }
self get_return_object() { return {std::coroutine_handle<promise_type>::from_promise(static_cast<promise_type&>(*this))}; }
};
template<typename T>
struct add_reference
{
using type = T&;
};
template<>
struct add_reference<void>
{
using type = void;
};
template<typename T>
using add_reference_t = typename add_reference<T>::type;
template<typename T = void>
class task
{
static constexpr bool is_void = std::is_same_v<T, void>;
public:
struct promise_type_nonvoid : promise_type_base<task>
{
std::optional<T> t_;
void return_value(T t)
{
t_ = std::move(t);
this->stack_->pop_back();
}
};
struct promise_type_void : promise_type_base<task>
{
void return_void()
{
this->stack_->pop_back();
}
};
struct promise_type : std::conditional_t<is_void, promise_type_void, promise_type_nonvoid>
{
using stack = promise_type_vars::stack;
};
private:
std::coroutine_handle<promise_type> h_;
public:
task(std::coroutine_handle<promise_type> h) : h_(h)
{
h_.promise().stack_ = std::make_shared<typename promise_type::stack>();
h_.promise().stack_->push_back(h_);
}
task(const task&) = delete;
task& operator=(const task&) = delete;
task(task&& other)
{
swap(h_, other.h_);
}
task& operator=(task&& other)
{
swap(h_, other.h_);
return *this;
}
~task()
{
if(h_)
{
h_.destroy();
h_ = {};
}
}
bool is_resumable() const { return h_ && !h_.done(); }
bool operator()() { return resume(); }
auto& stack() const { return *h_.promise().stack_; }
bool resume()
{
assert(is_resumable());
auto& s = *h_.promise().stack_;
auto back = s.back();
back.resume(); //execute suspended point
// at this point, back and s.back() could be different
if(back.done())
{
if(s.size() > 0)
{
// Not root, execute co_await return expression
resume();
}
else
{
if constexpr(!is_void)
{
assert(h_.promise().t_ && "Should return a value to caller");
}
}
}
return is_resumable();
}
// Make co_await'able (allow recursion and inner task as part of the parent task) ------------------------
bool await_ready() const
{
return false;
}
template<typename X>
bool await_suspend(std::coroutine_handle<X>& p)
{
// --- stack push
h_.promise().stack_ = p.promise().stack_;
p.promise().stack_->push_back(h_);
// ---
h_(); // never wait an inner function whether initially or finally (initially managed here)
if(!h_.done())
{
// the inner coroutine has at least one suspend point
return true;
}
else
{
return false; // don't wait if the coroutine is already ended (coroutine is NOOP)
}
}
T await_resume()
{
if constexpr(!is_void)
{
assert(h_.promise().t_ && "Should return a value in a co_wait expression");
return get();
}
}
add_reference_t<T> get()
{
if constexpr(!is_void)
{
return h_.promise().t_.value();
}
}
};
task<double> one()
{
std::cout << "one takes a coffee" << std::endl;
co_await std::suspend_always{};
co_return 1;
}
task<> noop()
{
std::cout << "the noop does something" << std::endl;
co_return;
}
task<short> two()
{
co_return co_await one() + co_await one();
}
task<int> foo()
{
int i = 0;
i += co_await one();
std::cout << "foo just run one()" << std::endl;
i += co_await two();
std::cout << "foo just run two()" << std::endl;
std::cout << "foo makes a pause after his work" << std::endl;
co_await std::suspend_always{};
co_await noop();
co_return i;
}
int main()
{
auto handle = foo();
for(int i = 1; handle(); ++i) {
std::cout << i << ". hi from main" << std::endl;
}
std::cout << "Result: " << handle.get() << std::endl;
return 0;
}
Output :
one takes a coffee
1. hi from main
foo just run one()
one takes a coffee
2. hi from main
one takes a coffee
3. hi from main
foo just run two()
foo makes a pause after his work
4. hi from main
the noop does something
Result: 3