/cpp20-coro-generator

Example of a C++20 coroutine implementing a Fibonacci Sequence generator (using clang++ v16.0.0)

Primary LanguageC++MIT LicenseMIT

Exploring C++20 coroutines

Introduction

A simple program implementing one specific capability of C++20 coroutines1 — a generator. The coro::generator<T> template class also supports C++20 range iteration, e.g., std::ranges::for_each() can be used to consume an instantiated generator-based function's output.

This generator example generates the Fibonacci Sequence up to some specified ceiling.

Description

Here is the function - its the use of co_yield that make it a C++20 coroutine (as opposed to an ordinary function):

    template<arithmetic T>
    generator<T> fibonacci(const T ceiling) {
      T j = 0;
      T i = 1;
      co_yield j;
      if (ceiling > j) {
        do {
          co_yield i;
          T tmp = i;
          i += j;
          j = tmp;
        } while (i <= ceiling);
      }
    }

The generator function's return value generator<T> is an iterator for the type of its template argument. In this programming example it is using the template class coro::generator<> which the implementation of is provided in generator.h.

NOTE: The template class coro::generator<> has been customized off of Rainer Grimm's implementation.2

Here is code that consumes generated values from fibonacci():

    const auto demo_ceiling = std::numeric_limits<double>::max() / 1'000.0f;

    auto iter = fibonacci(demo_ceiling);

    while(iter.next()) {
      const auto value = iter.getValue();
      std::cout << value << '\n';
    }

This will print out 1463 values of the Fibonacci Sequence.

The consuming code and the generator code are executing on the same thread context and yet the fibonacci() function enjoys a preserved local scope state as it executes and then resumes from co_yield. The generator function just falls out of the loop when the specified ceiling is exceeded to terminate itself - the consuming code will detect this in the while(iter.next()) {...} loop condition and fall out of the loop.

C++17 pmr allocators

The coro::generator<T> template class now uses C++17 pmr memory_resource allocators. By default the std::pmr::new_delete_resource allocator is used which relies on the global new and delete. It is also std::pmr::synchronized_pool_resource so is thread-safe. The function coro::set_pmr_mem_pool() can be used to set an alternative or custom pmr allocator. The helper class coro::fixed_buffer_pmr_allocator can be used to setup a stack-based, fixed-size buffer (or, say, a data segment fixed-sized buffer).

This program shows two cases of instantiating and invoking a generator where the function coro::set_pmr_mem_pool() is used to specify a stack-based pmr allocator.

Then the function coro::reset_default_pmr_mem_pool() can be invoked to reset the coro::generator<T> template class back to using the default allocator.

Building the program

The program has been built with cmake and with g++ version 12.1.0 or clang++ version 16.0.0. 3

NOTE: On my computer I installed version 16 of clang/llvm from a downloaded .tar.gz file; per the directory as to where I untarred to, I then had to update these symbolic links to reference the version 16 clang shared libraries:

/lib/x86_64-linux-gnu/libc++.so.1.0
/lib/x86_64-linux-gnu/libc++abi.so.1.0
/lib/x86_64-linux-gnu/libunwind.so.1.0

1: cppreference.com - Coroutines (C++20)

2: Rainer Grimm, Concurrency with Modern C++ (Leanpub, 2017 - 2019), 207-209.

3: Clang-LLVM downloads