Reputation: 1094
I gather (from here) that the coroutine types is broadly classified into 3:
generator<> , task<>, and lazy<>
My question is: What is the difference between the three if I'm wanting to decide on the return type?
For example: What would be the return class for a co-routine that lazy-loads a set of file handlers? My implementation would use task<FileHandler>
and generator<Filehandler>
to achieve the same.
I've looked this under the 'execution' section regarding the restrictions on the promise object
when it interacts with the coroutine. But I can only find implementation differences rather than methodology differences.
Upvotes: 2
Views: 2532
Reputation: 1094
I did some research and I believe the answer is as follows:
First, there is no lazy<>
class in C++. The https://en.cppreference.com/w/cpp/language/coroutines has it wrong. (Refer the draft for confirmation)
So, it's a matter of distinction between the return type of generator<T>
and task<T>
.
TLDR;
The easiest way to remember is:
generators
are associated withco_yield
; whereas,tasks
are associated withco_await
generator<T>
The co_yield mechanism associated with the generator class is exactly the same as what we would encounter in python (refer the docs) and also much similar to the thread_suspend
concept in operating system mechanisms.
You may choose to implement it synchronously or asynchronously. (Refer the cppcoro library for samples.)
A generator type (kind-of) looks like this:
struct generator {
struct promise_type;
using handle = std::coroutine_handle<promise_type>;
struct promise_type {
int current_value;
static auto get_return_object_on_allocation_failure() { return generator{nullptr}; }
auto get_return_object() { return generator{handle::from_promise(*this)}; }
auto initial_suspend() { return std::suspend_always{}; }
auto final_suspend() noexcept { return std::suspend_always{}; }
void unhandled_exception() { std::terminate(); }
void return_void() {}
auto yield_value(int value) {
current_value = value;
return std::suspend_always{};
}
};
bool move_next() { return coro ? (coro.resume(), !coro.done()) : false; }
int current_value() { return coro.promise().current_value; }
generator(generator const&) = delete;
generator(generator && rhs) : coro(rhs.coro) { rhs.coro = nullptr; }
~generator() { if (coro) coro.destroy(); }
private:
generator(handle h) : coro(h) {}
handle coro;
};
You would use the generator type as follows:
generator f() { co_yield 1; co_yield 2; }
task<T>
A task, on the other hand, associates with the co_await
expression. It requires the Awaitable<T>
and Awaiter<T>
concepts, so make sure you appropriately use the constraints associated with the two concepts. The Awaiter concept includes the constraints: await_ready
, await_suspend
, and await_resume
. The Awaitable concept has the constraints:(1) co_await
specialization/overload and (2) no overload of await_transform
.[ ref ]
The task type looks like this:
class task
{
public:
using promise_type = <unspecified>;
using value_type = T;
task() noexcept;
task(task&& other) noexcept;
task& operator=(task&& other);
task(const task& other) = delete;
task& operator=(const task& other) = delete;
bool is_ready() const noexcept;
Awaiter<T&> operator co_await() const & noexcept;
Awaiter<T&&> operator co_await() const && noexcept;
Awaitable<void> when_ready() const noexcept;
};
You may choose to use the concepts (perferrably my approach), or if you're not familiar with the existence of these concepts yet, then you may need to implement the associated constraints by yourself for your implementation.
Using tasks is as simple as [ref]:
task<> tcp_echo_server() {
char data[1024];
for (;;) {
size_t n = co_await socket.async_read_some(buffer(data));
co_await async_write(socket, buffer(data, n));
}
}
Upvotes: 1