Created
January 24, 2024 17:15
-
-
Save farhangamary/54eb275e1af3db5fb49e909082ac1a8f to your computer and use it in GitHub Desktop.
C++ 20 coroutines explained in comments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // 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