Skip to content

Instantly share code, notes, and snippets.

@farhangamary
Created January 24, 2024 17:15
Show Gist options
  • Select an option

  • Save farhangamary/54eb275e1af3db5fb49e909082ac1a8f to your computer and use it in GitHub Desktop.

Select an option

Save farhangamary/54eb275e1af3db5fb49e909082ac1a8f to your computer and use it in GitHub Desktop.
C++ 20 coroutines explained in comments
//
// Created by farhang on 21.01.24.
//
#include <iostream>
#include <coroutine>
#include <exception>
#include <cstdlib>
#define cot std::cout
#define le std::endl
/**
* The coroutine handle which is required for coroutine
* @tparam T the type of the value used inside the promise object for the coroutine.
* For example if the purpose of using coroutine is an unsigned int sequence generator, it can be stored inside the promise object
*/
template<typename T>
struct Generator {
struct promise_type;
struct promise_type {
//To hold the generated value
T val_;
//To point to a exception - later, the currently thrown one.
std::exception_ptr exceptionPtr;
//A coroutine handle should have a self-constructor function with the name get_return_object
Generator get_return_object() {
return Generator(std::coroutine_handle<promise_type>::from_promise(*this));
}
// Required methods to be a coroutine_handle
std::suspend_always initial_suspend() {
//C++ 20 designated initializer syntax.
//Returns an instance of std::suspend_always using default constructor
return {};
}
std::suspend_never final_suspend() noexcept { return {}; }
void unhandled_exception() {
//Get the currently thrown exception
exceptionPtr = std::current_exception();
}
/**
* yield_value function, to yield a value to the caller.
* When a coroutine encounters co_yield, the yield_value function is implicitly called
* to transfer control and pass a value to the caller.
* The passed value might be a lvalue or a rvalue, that's why the forwarding reference(&&)
* and perfect forwarding is used.
*
* @tparam From the type of the value getting yield - std::convertible_to is a C++ 20 'Concept'
* @param from the forwarding reference of the value getting passed
* @return
*/
template<std::convertible_to<T> From>
std::suspend_always yield_value(From &&from) {
val_ = std::forward<From>(from);
return {};
}
/**
* Where the co_return is called or the execution of the coroutine ends,
* then return_void function is called by compiler.
* It can have an argument, in this case the co_return should get invoked with a parameter.
*/
void return_void() {}
};
std::coroutine_handle<promise_type> ht_;
//Constructor
explicit Generator(std::coroutine_handle<promise_type> ht) : ht_(ht) {}
//Disabling the copy constructor
Generator(const Generator &) = delete;
//Destructor
~Generator() { ht_.destroy(); }
/**
* This is to be able to call the done() function of
* the instance of the coroutine_handle just using the () operator on the instance of the coroutine.
* This is used to check whether the generator has more values to yield.
*/
explicit operator bool() {
fill();
return !ht_.done();
}
/**
* Is used to retrieve the next value from the generator.
* It advances the coroutine and returns the next yielded value.
*/
T operator()() {
fill();
full_ = false;
return std::move(ht_.promise().val_); //The move casts its argument to a rvalue reference.
}
private:
//A helper flag for checking if a value is generated
bool full_ = false;
/**
* A helper method used inside "operator bool()" and "T operator()()".
* It checks whether the generator has already produced a value (full_ flag).
* If not, it advances the coroutine to the next state (ht_()) and checks for any exceptions thrown during the coroutine execution.
* If an exception is caught, it is rethrown.
* Finally, it sets the full_ flag to true to indicate that the generator has produced a value.
*/
void fill() {
if (!full_) {
ht_();
if (ht_.promise().exceptionPtr) {
std::rethrow_exception(ht_.promise().exceptionPtr);
}
full_ = true;
}
}
};
//The coroutine itself
Generator<unsigned> counter() {
for (unsigned i = 0; i < 3; ++i) {
/**
* co_yield i is equal to "co_await promise.yield_value(i)"
* and in "co_await r", r should be an awaitable and compiler invokes await_suspend(coroutine_handle) of that awaitable.
*/
co_yield i;
}
// End of function or co_return; which is indeed promise.return_void();
// (co_return value; is indeed promise.return_value(value))
}
int main() {
//Create an instance of the coroutine
auto generator = counter();
/**
* "generator" as the while argument is a use of operator bool()
* and "generator()" is the usage of T operator()()
*/
while (generator) {
unsigned value = generator();
if(!generator.ht_.done())
cot << "Generated value: " << value << le;
}
return EXIT_SUCCESS;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment