| by Arround The Web | No comments

C++ Coroutines Examples

Coroutines provide a language feature that enables you to write the asynchronous code in a more organized and linear fashion, promoting a structured and sequential approach. They give a mechanism to pause and restart a function’s execution at particular instances without stopping the whole thread. Coroutines are helpful when handling tasks that require waiting for I/O operations such as reading from a file or sending a network call.

Coroutines are based on the concept of generators where a function can yield values and later be resumed to continue the execution. Coroutines provide a powerful tool to manage the asynchronous operations and can greatly improve the overall quality of your code.

Uses of Coroutines

Coroutines are needed for several reasons in modern programming, particularly in languages like C++. Here are some key reasons why coroutines are beneficial:

Coroutines provide an elegant solution to asynchronous programming. They make it possible to create a code that appears sequential and blocking which is simpler to reason about and comprehend. Coroutines can suspend their execution at specific points without blocking the threads, enabling a parallel operation of other tasks. Because of this, the system resources may be used more effectively, and responsiveness is increased in applications that involve the I/O operations or waiting for external events.

They might make the code easier to understand and maintain. By eliminating the complex callback chains or state machines, coroutines enable the code to be written in a more linear and sequential style. This improves the code organization, reduces nesting, and makes the logic easy to comprehend.

Coroutines provide a structured way to handle the concurrency and parallelism. They allow you to express the complex coordination patterns and asynchronous workflows using a more intuitive syntax. Unlike traditional threading models where the threads might be blocked, coroutines can free up the system resources and enable an efficient multitasking.

Let’s create some examples to demonstrate the implementation of coroutines in C++.

Example 1: Basic Coroutines

The basic coroutines example is provided in the following:

#include <iostream>

#include <coroutine>

struct ThisCorout {

  struct promise_type {

    ThisCorout get_return_object() { return {}; }

    std::suspend_never initial_suspend() { return {}; }

    std::suspend_never final_suspend() noexcept { return {}; }

    void unhandled_exception() {}

    void return_void() {}

  };

    bool await_ready() { return false; }

    void await_suspend(std::coroutine_handle<> h) {}

    void await_resume() { std::cout << "The Coroutine is resumed." << std::endl; }

};

ThisCorout foo() {

    std::cout << "The Coroutine has started." << std::endl;

    co_await std::suspend_always{};

    co_return;

    }

    int main() {

    auto cr = foo();

    std::cout << "The Coroutine is created." << std::endl;

    cr.await_resume();

    std::cout << "Coroutine finished." << std::endl;

    return 0;

}

Let’s go through the previously provided code and explain it in detail:

After including the required header files, we define the “ThisCorout” struct that represents a coroutine. Inside the “ThisCorout”, another struct which is “promise_type” is defined that handles the coroutine promise. This struct provides various functions that are required by the coroutine machinery.

Inside the brackets, we utilize the get_return_object() function. It returns the coroutine object itself. In this instance, it returns an empty “ThisCorout” object. Then, the initial_suspend() function is invoked which determines the behavior when the coroutine is first started. The std::suspend_never means that the coroutine should not be suspended initially.

After that, we have the final_suspend() function which determines the behavior when the coroutine is about to finish. The std::suspend_never means that the coroutine should not be suspended before its finalization.

If a coroutine throws an exception, the unhandled_exception() method is invoked. In this example, it’s an empty function, but you can handle the exceptions as needed. When the coroutine terminates without yielding a value, the return_void() method is invoked. In this case, it’s also an empty function.

We also define three member functions within “ThisCorout”. The await_ready() function is called to check if the coroutine is ready to resume the execution. In this example, it always returns false which indicates that the coroutine is not ready to resume immediately. When the coroutine is going to be suspended, the method await_suspend() is called. Here, it’s an empty function which means that no suspension is necessary. The program calls the await_resume() when the coroutine is resumed after suspension. It just outputs a message which states that the coroutine has been resumed.

The next lines of the code define the foo() coroutine function. Inside foo(), we begin by printing a message which states that the coroutine has started. Then, co_await std::suspend_always{} is used to suspend the coroutine and indicates that it can be resumed at a later point. The co_return statement is used to finish the coroutine without returning any value.

In the main() function, we construct an object “cr” of type “ThisCorout” by calling foo(). This creates and starts the coroutine. Then, a message which states that the coroutine has been created is printed. Next, we call the await_resume() on the “cr” coroutine object to resume its execution. Inside the await_resume(), the “The Coroutine is resumed” message is printed. Finally, we display a message which states that the coroutine is complete before the program terminates.

When you run this program, the output is as follows:

Example 2: Coroutine with Parameters and Yielding

Now, for this illustration, we provide a code that demonstrates the use of coroutines with parameters and yielding in C++ to create a generator-like behavior to produce a sequence of numbers.

#include <iostream>

#include <coroutine>

#include <vector>

struct NEWCoroutine {

  struct p_type {

    std::vector<int> values;

    NEWCoroutine get_return_object() { return {}; }

    std::suspend_always initial_suspend() { return {}; }

    std::suspend_always final_suspend() noexcept { return {}; }

    void unhandled_exception() {}

    void return_void() {}

    std::suspend_always yield_value(int value) {

    values.push_back(value);

    return {};

    }

};

    std::vector<int> values;

    struct iterator {

        std::coroutine_handle<> coro_handle;

        bool operator!=(const iterator& other) const { return coro_handle != other.coro_handle; }

        iterator& operator++() { coro_handle.resume(); return *this; }

        int operator*() const { return coro_handle.promise().values[0]; }

};

  iterator begin() { return iterator{ std::coroutine_handle<p_type>::from_promise(promise()) }; }
 
 iterator end() { return iterator{ nullptr }; }

  std::coroutine_handle<p_type> promise() { return
 std::coroutine_handle<p_type>::from_promise(*this); }

};

 NEWCoroutine generateNumbers() {

  co_yield 5;

  co_yield 6;

  co_yield 7;

}

  int main() {

    NEWCoroutine nc = generateNumbers();

    for (int value : nc) {

        std::cout << value << " ";

    }

  std::cout << std::endl;

  return 0;

}

In the previous code, the NEWCoroutine struct represents a coroutine-based generator. It contains a nested “p_type” struct that serves as the promise type for the coroutine. The p_type struct defines the functions that are required by the coroutine machinery such as get_return_object(), initial_suspend(), final_suspend(), unhandled_exception(), and return_void(). The p_type struct also includes the yield_value(int value) function which is used to yield the values from the coroutine. It adds the provided value to the values vector.

The NEWCoroutine struct includes the std::vector<int> member variable called “values” which represents the generated values. Inside the NEWCoroutine, there is a nested struct iterator that allows to iterate over the generated values. It holds a coro_handle which is a handle to the coroutine and defines the operators such as !=, ++, and * for iteration.

We use the begin() function to create an iterator at the start of the coroutine by obtaining the coro_handle from the p_type promise. Whereas the end() function creates an iterator that represents the end of the coroutine and is constructed with a nullptr coro_handle. After that, the promise() function is utilized to return the promise type by creating a coroutine_handle from the p_type promise. The generateNumbers() function is a coroutine that yields three values – 5, 6, and 7 – using the co_yield keyword.

In the main() function, an instance of NEWCoroutine named “nc” is created by invoking the generateNumbers() coroutine. This initializes the coroutine and captures its state. A range-based “for” loop is used to iterate over the values of “nc”, and each value is printed which are separated by a space using the std::cout.

The generated output is as follows:

Conclusion

This article demonstrates the utilization of coroutines in C++. We discussed two examples. For the first illustration, the basic coroutine is created in a C++ program using the coroutine functions. While the second demonstration was carried out by utilizing the coroutines with parameters and yielding to generate a generator-like behavior to create a sequence of numbers.

Share Button

Source: linuxhint.com

Leave a Reply