c++std-rangesc++-coroutinec++23

Implement views::concat using C++ coroutine?


I recall reading Eric Niebler's quote somewhere: "Coroutines make it trivial to define your own ranges", which led me to try using coroutines to implement the well-known views::concat that is not yet adopted by C++23.

Since there is currently no compiler implementing std::generator, I plan to use the famous cppcoro library instead. My first attempt was to intuitively expand the lambda using the fold expression (simplified to only support ranges of ints):

#include <ranges>
#include <cppcoro/generator.hpp>

template<ranges::input_range... Rs>
auto concat(Rs&&... rngs) -> cppcoro::generator<int> 
{
  ([](auto& rng) -> cppcoro::generator<int> {
    for (auto elem : rng)
      co_yield elem;
  }(rngs), ...);
}

Unfortunately, that's not how coroutines work, the above just expands the lambda and discards the returned generator.

I also tried using syntactic sugar such as co_yield rngs...;, but this is obviously not valid syntax.

My final solution was to create a coroutine lambda that yields one element at a time (which can be simplified with ranges::elements_of in C++23). Since the return value of the coroutine is a generator, I can save different generators by std::array, and then apply views::join to this nested range to concat its elements:

template<ranges::input_range... Rs>
auto concat(Rs&&... rngs) -> cppcoro::generator<int> 
{
  auto lambda = [](auto& rng) -> cppcoro::generator<int> {
    for (auto elem : rng)
      co_yield elem;
  };
  std::array nested_rng{lambda(rngs)...};
  for (auto elem : nested_rng | views::join)
    co_yield elem;
}

This works fine for my testcase (Demo).

However, such an approach requires additional use of std::array to store different generators and depends on views::join, I wonder if there is a more concise and efficient way to achieve this?


Solution

  • You don't need the array (or views::join), once you have the lambda you can fold over it:

    template<std::ranges::input_range... Rs>
    auto concat(Rs&&... rs) -> std::generator<int&> 
    {
        (co_yield std::ranges::elements_of(rs), ...);
    }
    

    I'm not sure if you can do any better until we get expansion statements.

    Demo (with gcc/libstdc++).


    Note generator<int&> and not generator<int> because in the example I'm using vector<int>, whose reference type is int&... but generator<int>'s reference type is int&&. Attempting to directly use elements_of(rs) doesn't work because the reference type isn't implicitly convertible.