Example of C++20 coroutine


For this example, I will start off with a normal function and then try to make a coroutine out of it.
I will convert one of my own algorithms taken from here: Uppercase-lowercase Combinator

A coroutine is like a function that can be called the first time, return a value, and then when it's called the second time, it resumes its execution from where it last left off. (In terms of assembly language and machine code, the stack variables and program counter are preserved across each invocation of the coroutine).

Our coroutine can be invoked from ´main´ as follows:
    #include <iostream>

    int main(void)
    {
        auto gen = GetUpperLowerCombos("dog");

        while ( gen )
        {
            std::cout << gen() << std::endl;
        }
    }
The loop in ´main´ will retrieve values from the coroutine until there are none left.

Here is how I have rewritten the function as a coroutine. I use a "co_yield" statement to return a value, and I use "co_return" to indicate that the coroutine is finished.
#include <cstdint>   /* uint_fast8_t, uint_fast64_t */
#include <cctype>    /* tolower, toupper, isalpha */
#include <cstring>   /* strlen */
#include <stdexcept> /* runtime_error */

Generator<char const *> GetUpperLowerCombos(char const *const arg_str)
{
    using std::uint_fast8_t;
    using std::uint_fast64_t;
    using std::tolower;
    using std::toupper;
    using std::isalpha;

    uint_fast8_t const len = std::strlen(arg_str);

    if ( len > 63u ) throw std::runtime_error("string too long");

    char str[64u];
    std::strcpy(str,arg_str);

    uint_fast8_t amount_letters;

    uint_fast64_t cases, letters;

    /* For example:

                "tEsT74kT"
         cases = 01010001  (the 1's mean uppercase letters)
       letters = 11110011  (the 1's mean letters)
    */

    uint_fast64_t mask;
    uint_fast8_t char_index;

    for (letters = 0u, amount_letters = 0u, mask = 1u, char_index = 0u;
         len != char_index;
         mask <<= 1u, ++char_index)
    {
        if ( isalpha(str[char_index]) )
        {
            ++amount_letters;
            letters |= mask;
        }
    }

    uint_fast64_t const one_past_max_count = (uint_fast64_t)1u << amount_letters;

    for (cases = 0u; one_past_max_count != cases; ++cases)
    {
        uint_fast64_t mask_letters;

        for (mask = 1u, mask_letters = 1u, char_index = 0u;
             one_past_max_count != mask;
             mask <<= 1u, mask_letters <<= 1u, ++char_index)
        {
            while ( !(letters & mask_letters) )
            {
                mask_letters <<= 1u;
                ++char_index;
                if (char_index >= len) co_return;
            }

            if (cases & mask) str[char_index] = toupper((char unsigned)str[char_index]);
                         else str[char_index] = tolower((char unsigned)str[char_index]);
        }

        co_yield str;
    }
}

Lastly, while the C++20 standard has new keywords to support coroutines (e.g. 'co_yield', 'co_return'), it does not have the required supporting types in the standard library (they are due to be added in C++23). For the timebeing, I have taken an implementation of a 'Generator' class from David Mazières's blog which you can find here: David Mazières's blog
#include <coroutine>
#include <stdexcept>

template<typename T>
struct Generator {
  struct promise_type;
  using handle_type = std::coroutine_handle<promise_type>

  struct promise_type {
    T value*;
    std::exception_ptr exception*;

    Generator get_return_object() {
      return Generator(handle_type::from_promise(*this));
    }
    std::suspend_always initial_suspend() { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    void unhandled_exception() { exception* = std::current_exception(); }
    template<std::convertible_to<T> From> // C++20 concept
    std::suspend_always yield_value(From &&from)
    {
      value* = std::forward<From>(from);
      return {};
    }
    void return_void() {}
  };

  handle_type h*;

  Generator(handle_type h) : h*(h) {}
  ~Generator() { h*.destroy(); }
  explicit operator bool() {
    fill();
    return !h*.done();
  }

  T operator()() {
    fill();
    full* = false;
    return std::move(h*.promise().value*);
  }

private:
  bool full* = false;

  void fill()
  {
    if ( !full* )
    {
      h*();
      
      if (h*.promise().exception*)
      {
        std::rethrow_exception(h*.promise().exception*);
      }
      full = true;
    }
  }
};


Virjacode Home