How do I reuse a C++23 `std::generator` instance as a range in multiple range expressions?
Asked Answered
A

2

5

I'm trying to learn C++ ranges. As far as I can see, the simplest (and the only, short of implementing a custom range view class) way to create a range view object that generates a custom sequence is to use C++23' std::generator<>:

std::generator<char> letters_gen(char start)
{
    for (;;) co_yield start++;
}

How do I use a std::generator object (as created by invoking letters_gen()) in multiple expressions involving range operations? E.g. if I want to collect 10 letters into a vector, and then 15 more letters into another vector:

int main()
{
    auto letters = letters_gen('a');
    auto x = letters | std::views::take(10) | std::ranges::to<std::vector>();
    auto y = letters | std::views::take(15) | std::ranges::to<std::vector>();
}

This does not compile:

main.cc:150:26: error: no match for ‘operator|’ (operand types are ‘std::generator<char>’ and ‘std::ranges::views::__adaptor::_Partial<std::ranges::views::_Take, int>’)

What is the proper way, if any, to achieve the desired effect?

Angers answered 30/9, 2024 at 16:44 Comment(3)
If you just want each element to be one more than the previous std::ranges::iota might work for you.Fritillary
@Fritillary The generator in the question was a minimal example, not something to infer additional constraints from.Angers
Perhaps you could make a wrapper that holds the iterator, like godbolt.org/z/G5cqezE1hRositaroskes
B
10

You don't.

A generator is an input range, you can only use it one time. One of the ways the library seeks to prevent misuse is to make it move-only instead of copyable - hence the complaint about trying to copy it (ranges compile errors are notoriously useless). It is undefined behavior to even call begin() twice on the range.

There's not really a good way to do this within the library I don't think. You might try to work around the copying issue by doing something like this (which additionally ensures that both algorithms use the same generator):

auto x = ranges::ref_view(letters) | views::take(10) | ranges::to<vector>();
auto y = ranges::ref_view(letters) | views::take(15) | ranges::to<vector>();

This is undefined behavior, but even setting that aside this will give you the surprising outcome that x contains the letters from a thru j as desired but y contains l thru z intead of k thru y due to the way that take interacts with input ranges (see my CppNow talk).

You'd have to write something more by hand:

// we have to use iterators and preserve them
auto it = letters.begin();

std::vector<char> x;
for (size_t count = 0; count < 10 && it != letters.end(); ++it, ++count) {
    x.push_back(*it);
}

std::vector<char> y;
for (size_t count = 0; count < 15 && it != letters.end(); ++it, ++count) {
    y.push_back(*it);
}

Bartolomeo answered 30/9, 2024 at 17:2 Comment(3)
"It is undefined behavior to even call begin() twice on the range." ← could you please elaborate on this, or where I can read about it (and presumably other similar limitations)? I couldn't find anything saying or implying that on cppreference.Angers
@Bartolomeo Excellent answer! But not everything is so pessimistic. Yes, we won't be able to use views in this case, but we can use algorithms – copy_n and ranges::copy_n.Predilection
@Angers So in general, an input-only range need not support calling begin() twice. As std::generator does notBartolomeo
P
1

To Barry's excellent answer I would like to add the following. Not everything is so pessimistic. Yes, we won't be able to use views in this case, but we can use algorithms – std::copy_n and std::ranges::copy_n:

    std::vector<char> x, y;
    auto              letters = letters_gen('a');
    std::copy_n(letters.begin(), 10, std::back_inserter(x));
    std::copy(x.begin(), x.end(), std::ostream_iterator<char>(std::cout, ", "));
    std::cout << '\n';
    std::copy_n(letters.begin(), 15, std::back_inserter(y));
    std::copy(y.begin(), y.end(), std::ostream_iterator<char>(std::cout, ", "));

or:

    std::vector<char> x, y;
    auto              letters = letters_gen('a');
    auto              res     = std::ranges::copy_n(letters.begin(), 10, std::back_inserter(x));
    std::ranges::copy(x, std::ostream_iterator<char>(std::cout, ", "));
    std::cout << '\n';
    std::ranges::copy_n(res.in, 15, std::back_inserter(y));
    std::ranges::copy(y, std::ostream_iterator<char>(std::cout, ", "));
Predilection answered 1/10, 2024 at 13:28 Comment(2)
I just hoped to be able to use std::ranges::to() instead of the back_inserter because it's pretty convenient.Angers
Not the first one - which still calls letters.begin() twice. The second approach works though - except you have to std::move(res.in) because it's not copyable (and would be nice to not clutter the answer with the irrelevant copy to std::cout)Bartolomeo

© 2022 - 2025 — McMap. All rights reserved.