Updated April 4, 2023
Introduction to Coroutines C++
Coroutines in C++ is a function that gets resumed when required, once suspended at the time of execution. Coroutines are generally known with an extension to subroutines although there exist some minor distinctions between both. Coroutines are used in many phases of development like the execution of code asynchronously with a set of proper sequential code into place. Coroutines are basically stacked less in nature due to which it is used for returning the result to the caller by resuming the required coroutine that is again stored in the separate location or stack that is already defined.
Syntax:
The syntax flow is considered a Coroutine if the function works in the manner where it gets resumed with the below function of the snippet:
This function makes use of the keyword co_yield for resuming and execution.
generator<int> iota (int p_1 = 0)
{
while(true)
co_yield p_1++;
}
How do Coroutines work in C++?
There is a proper working for Coroutines in C++ which will be illustrated below:
- As described before coroutines are the function that is stackless in nature and has the capability to resume at a later point of time even when the primary execution gets completed.
- How it works mainly is it calls the main function and then the data present gets saved in another location that is not related to the primary location where the actual invocation gets started with.
- Coroutines allow some set of sequential code to execute asynchronously without any explicit call-making algorithms filled with lazy computation and infinite sequence with some other utilization.
- Every coroutines used as a function must have an associated return type for its making.
- Although there attached is a parallel variadic argument, return statements or placeholders it helps in making a perfect coroutine still exist some restrictions.
- According to restriction constructors, destructors, constexpr function and main function cannot form perfect coroutines.
- Let’s deviate the focus towards its execution:
Coroutine Execution Paradigm:
- Each coroutine has an association with it which are as follows:
- The promise object: Promise object is used from within the function which is used for submitting its result and exception through this promise object.
- Coroutine state: Coroutine state is again another state which works internally with allocated heap where the heap is available in an optimized manner. This state object again consists of object that should have following characteristics:
- Parameters that are available (all the copied values taken into consideration implicitly)
- Local variables and temporaries whose values are having little timestamp and scope until the termination of current suspension point.
- Promise object for manipulation within.
- Representation with proper state and value for the local variables in scope so that the value which needs to be resumed understands where to resume from continue and then destroy.
Coroutine handles:
- Coroutine handles are majorly responsible for manipulating the execution and compilation from the outside scope of coroutine frame.
- Coroutine handles have non-handling handlers where the handle is used for resuming the execution of coroutine and then simultaneously delete the coroutine from the entire frame.
- All these three associated components play major role in the entire coroutine execution paradigm followed by some scenarios which makes use of these components:
- When a coroutine begins its execution then it performs following action where:
- It allocates coroutine state object by using new operator.
- It then copies all the parameters present to the coroutine state depicting by-value parameter, by-reference parameter, followed by call of a reference which is remaining.
- It then calls for the constructor of promise object when promise object have constructor where it takes all the arguments into consideration.
- Calls for promise object to get and return the function which gets placed in a local variable followed which it helps in returning suspension point.
- Co_awaits as part of coroutine gets resumed and then it gets resumed with start of the coroutine.
- Once coroutine reaches the suspension point then in that case the caller object gets returned to actual caller after implicit conversion to the actual return type which was present at the time of initiation.
- There exists another scenario where the coroutine state finally gets destroyed and gets terminated through co_return or some unwanted uncaught exceptions handled by its handle.
Examples
Here are the following examples mentioned below:
Example #1
This program demonstrates the coroutines greedy generator where the program is responsible for the function to return to all integers starting from begin to end including generator numbers as shown in the output.
#include <iostream>
#include <vector>
std::vector<int> get_greedy_Numbers(int begin, int end, int inc = 1) {
std::vector<int> greedy_numbers;
for (int j_0 = begin; j_0 < end; j_0 += inc) {
greedy_numbers.push_back(j_0);
}
return greedy_numbers;
}
int main() {
std::cout << std::endl;
const auto greedy_numbers= get_greedy_Numbers(-5, 10);
for (auto n: greedy_numbers) std::cout << n << " ";
std::cout << "\n\n";
for (auto n: get_greedy_Numbers(1, 123, 8)) std::cout << n << " ";
std::cout << "\n\n";
}
Output:
Example #2
This program demonstrates the simplest of coroutine that is used for storing for values that returns the output with coroutine which is an asynchronous call by returning future value in function as shown in the output below.
#include <coroutine>
#include <iostream>
#include <memory>
template<typename T_0>
struct Future_Date {
std::shared_ptr<T_0> value;
Future_Date(std::shared_ptr<T_0> p_1): value(p_1) {}
~Future_Date() { }
T_0 get() {
return *value;
}
struct promise_type {
std::shared_ptr<T_0> ptr = std::make_shared<T_0>();
~promise_type() { }
Future_Date<T_0> get_return_object() {
return ptr;
}
void return_value(T_0 k) {
*ptr = k;
}
std::suspend_never initial_suspend() {
return {};
}
std::suspend_never final_suspend() noexcept {
return {};
}
void unhandled_exception() {
std::exit(1);
}
};
};
Future_Date<int> createFuture_0() {
co_return 2022;
}
int main() {
std::cout << '\n';
auto fut_1 = createFuture_0();
std::cout << "fut_1.get(): " << fut_1.get() << '\n';
std::cout << '\n';
}
Output:
Conclusion
C++ coroutines are quite advantageous in terms of programming requirements where there is a need for functions to perform implicit calling and manipulation. It gives developers an edge to perform calling and heap allocation which in turn aids for memory allocation. Coroutines in C++ are stackless in nature which means the stacks are handled independently for every invoked call.
Recommended Articles
This is a guide to Coroutines C++. Here we discuss How do Coroutines work in C++ along with the examples and outputs. You may also have a look at the following articles to learn more –