c++std-rangesc++23c++-coroutine

can a range of generators be joined


Is it possible to have a range of generators and if yes, is it possible to join them ?

#include <algorithm>
#include <ranges>
#include <vector> 
#include <cstdio>
#include <generator>
#include <iostream>
using namespace std;
 
template<typename T>
struct Tree
{
    T value;
    Tree *left{}, *right{};
 
    std::generator<const T&> traverse_inorder() const
    {
        if (left)
            co_yield std::ranges::elements_of(left->traverse_inorder());
 
        co_yield value;
 
        if (right)
            co_yield std::ranges::elements_of(right->traverse_inorder());
    }
};
 
int main()
{
    Tree<char> tree1[]
    {
                                    {'D', tree1 + 1, tree1 + 2},
        //                            │
        //            ┌───────────────┴────────────────┐
        //            │                                │
                    {'B', tree1 + 3, tree1 + 4},       {'F', tree1 + 5, tree1 + 6},
        //            │                                │
        //  ┌─────────┴─────────────┐      ┌───────────┴─────────────┐
        //  │                       │      │                         │
          {'A'},                  {'C'}, {'E'},                    {'G'}
    };

    generator<const char&> gen = tree1->traverse_inorder();
    for (char x : gen)
        std::cout << x << ' ';
    std::cout << '\n';

    generator<const char&> gen3[3]={gen,gen,gen};
    for (char x : gen3 | views::join)
        std::cout << x << ' ';
}
prog.cc: In function 'int main()':
prog.cc:47:48: error: use of deleted function 'std::generator<_Ref, _Val, _Alloc>::generator(const std::generator<_Ref, _Val, _Alloc>&) [with _Ref = const char&; _Val = void; _Alloc = void]'
   47 |     generator<const char&> gen3[3]={gen,gen,gen};
      |                                                ^
In file included from prog.cc:5:
/opt/wandbox/gcc-head/include/c++/16.0.0/generator:712:7: note: declared here
  712 |       generator(const generator&) = delete;
      |       ^~~~~~~~~
prog.cc:47:48: note: use '-fdiagnostics-all-candidates' to display considered candidates
   47 |     generator<const char&> gen3[3]={gen,gen,gen};
      |                                                ^
prog.cc:47:48: error: use of deleted function 'std::generator<_Ref, _Val, _Alloc>::generator(const std::generator<_Ref, _Val, _Alloc>&) [with _Ref = const char&; _Val = void; _Alloc = void]'
/opt/wandbox/gcc-head/include/c++/16.0.0/generator:712:7: note: declared here
  712 |       generator(const generator&) = delete;
      |       ^~~~~~~~~
prog.cc:47:48: note: use '-fdiagnostics-all-candidates' to display considered candidates
   47 |     generator<const char&> gen3[3]={gen,gen,gen};
      |                                                ^
prog.cc:47:48: error: use of deleted function 'std::generator<_Ref, _Val, _Alloc>::generator(const std::generator<_Ref, _Val, _Alloc>&) [with _Ref = const char&; _Val = void; _Alloc = void]'
/opt/wandbox/gcc-head/include/c++/16.0.0/generator:712:7: note: declared here
  712 |       generator(const generator&) = delete;
      |       ^~~~~~~~~
prog.cc:47:48: note: use '-fdiagnostics-all-candidates' to display considered candidates
   47 |     generator<const char&> gen3[3]={gen,gen,gen};
      |                                                ^

Solution

  • Can ranges of std::generator be joined? Definitely. Here's how your example can be easily fixed

        generator<const char&> gen3[3]={
            tree1->traverse_inorder(), 
            tree1->traverse_inorder(),
            tree1->traverse_inorder()
        };
        for (char x : gen3 | views::join)
            std::cout << x << ' ';
    

    A std::generator isn't copyable, the coroutine state can only be owned by a single std::generator object. It's a sane default. So the easy fix is just create three distinct coroutines from your tree.

    If you have pre-existing generators you can wrap them in std::ranges::ref_view

    std::ranges::ref_view<generator<const char&>> gen3[3]={gen1, gen2, gen3};
    

    That is of course assuming you own them, they aren't past their final suspend point, and they are all distinct object. That is also why you can't fix your own attempt by trying this. The reference semantics will make it refer to the same generator. I.e. this

    std::ranges::ref_view<generator<const char&>> gen3[3]={gen, gen, gen};
    

    will fail, since it will attempt to resume gen after it's fully exhausted the first time.