7

Since the std::generator is making it into CPP23, I am playing around with MSVC's incomplete version.

However, I notice that it seems lose exactly one yield when used with std::views::take. Here is the example:

#include <iostream>
#include <ranges>

#include <experimental/generator>

std::experimental::generator<int> GeneratorFn(void) noexcept
{
    co_yield 1;
    co_yield 2;
    co_yield 3;
    co_yield 4;
    co_yield 5;
    co_yield 6;
    co_yield 7;
    co_yield 8;
    co_yield 9;
    co_return;
}

int main(int argc, char** args) noexcept
{
    auto Ret = GeneratorFn();
    for (auto&& i : Ret | std::views::take(2))
        std::cout << i << '\n';
    for (auto&& i : Ret | std::views::take(3))
        std::cout << i << '\n';
    for (auto&& i : Ret | std::views::take(4))
        std::cout << i << '\n';
}

The output of this code would be

1
2
4
5
6
8
9

and clearly, the 3 and 7 is missing. It seems like std::views::take drops the last value the generator yields.

Is this normal and to be expected in the formal version of C++23?
(Try online: https://godbolt.org/z/v6MModvaz)

Hydrogen
  • 301
  • 1
  • 8
  • Why not report the problem to Microsoft? – Phil1970 Aug 02 '22 at 12:10
  • 2
    @Phil1970 Because generally it's VERY likely that this is my misunderstand of the standard instead of MS's. They're THE big tech in the field. – Hydrogen Aug 02 '22 at 12:24
  • 1
    It might be unavoidable: "`std::generator` is a move-only `view` which models `input_range` and has move-only iterators." The `end` in the first `take` eats an element – Caleth Aug 02 '22 at 12:34
  • 1
    This is the expected behavior, which is basically equivalent to [this](https://godbolt.org/z/Y1Mv1Y8h7). – 康桓瑋 Aug 02 '22 at 12:36
  • @Caleth Can I "bypass" this "feature" somehow? – Hydrogen Aug 02 '22 at 13:00
  • 1
    I don't know offhand what the contract on the experimental one is, but as far as the C++23 one is concerned, it is a precondition violation (and therefore undefined behavior) to call `begin` multiple times on the same `generator`. – T.C. Aug 03 '22 at 15:32

2 Answers2

9

std::generator is an input_range, its begin() does not guarantee equality-preserving:

auto Ret = GeneratorFn();
std::cout << *Ret.begin() << "\n"; // 1
std::cout << *Ret.begin() << "\n"; // 2

When your first for-loop finishes, Ret's iterator has already incremented to the value 3. When you apply views::take to Ret in the second for-loop, this will call Ret's begin again, and the iterator return by begin will be the next value 4.

If you don't want to discard the value of the end iterator, you can reuse the last end iterator like this

auto Ret = GeneratorFn();
auto c = std::views::counted(Ret.begin(), 2);
for (auto i : c)
  std::cout << i << '\n';
for (auto i : std::views::counted(c.begin(), 3))
  std::cout << i << '\n';
for (auto i : std::views::counted(c.begin(), 4))
  std::cout << i << '\n';

Demo

康桓瑋
  • 33,481
  • 5
  • 40
  • 90
  • Thanks for your explanation! I aware the ```begin()``` would be different on each call, hence my original intention is to take the first 2 values into the first range, the next 3 into the second range, etc. Is there any alternative way to make my design work? – Hydrogen Aug 02 '22 at 13:05
  • Thank you so much! This is exactly what I wanted to do! – Hydrogen Aug 02 '22 at 13:09
3

Pause-resume type behaviours want to be implemented with stateful function objects. Before you had to return std::vectors but now you can use std::generator instead

template<typename Gen>
auto TakeAdaptor(Gen&& gen)
{
    return [gen = std::move(gen)](int count) mutable 
        -> std::experimental::generator<int>
    {
        auto i = 0;
        if (not (i++ < count))
            co_return;
        for (auto e : gen)
        {
            co_yield e;
            if (not (i++ < count))
                break;
        }
    };
}

int main(int argc, char** args) noexcept
{
    auto take = TakeAdaptor(GeneratorFn());
    for (auto i : take(2))
      std::cout << i << '\n';
    for (auto i : take(3))
      std::cout << i << '\n';
    for (auto i : take(4))
      std::cout << i << '\n';
}

https://godbolt.org/z/x4957vr8z

Tom Huntington
  • 2,260
  • 10
  • 20
  • 1
    This alternative solution looks cool! Could you explain a bit about what the ```std::move```is doing in the lambda capture? Much appreciated! – Hydrogen Aug 05 '22 at 14:47
  • @Hydrogen `std::generator` i.e. the result of `GeneratorFn()` is move only (deleted copy constructor) so we must move it into the lambda rather than copy it – Tom Huntington Aug 05 '22 at 22:28
  • @Hydrogen also note that you could also leave the `std::generator` on the stack and accept it by reference to a stateless adaptor function https://godbolt.org/z/cnP9d5oha – Tom Huntington Aug 05 '22 at 23:37
  • 1
    Also T.C. points you it's undefined behaviour to call begin multiple times on the generator, so technically I should replace the range-for loops with loops over raw iterators. But in reality things get done by programming off observed behaviour – Tom Huntington Aug 05 '22 at 23:43
  • Thanks for your detailed explanation and reminder! I would try it by myself to do the replacement. And I actually kinda agree with you that "to a specific compiler everything is de facto well-defined". – Hydrogen Aug 06 '22 at 14:32
  • *"to a specific compiler everything is de facto well-defined"*. I wouldn't say that. Rather that programming is empirical, if it passes the compile and run test, then its observed behaviour is correct. It's impractical to precede any other way. But compiler and library writers have a different measure of correctness, and well-defined (by the standard) is the vocabulary of their correctness – Tom Huntington Aug 06 '22 at 22:40
  • Thanks for your reply. Could your share your opinion yet again if I change my wording to *"to a specific compiler version and a specific STL impl. version, everything is de facto well-defined."* ? I feel this wording patches all your bug reports. XD – Hydrogen Aug 08 '22 at 01:50
  • 1
    `well-defined` already means well defined according to the standard. It's like using literally to mean figuratively. *"The observed behaviour is correct"* is a better thing to say. You probably don't yet appreciate the process of standardization in computer science – Tom Huntington Aug 08 '22 at 01:59
  • Thanks for the clarification of the wording. Yes I am not a computer scientist but a chemist, thus the word "undefined behavior" just making no sense to me according to my background knowledge. I've watched several videos explaining this terminology but it still confusing me at this point – Hydrogen Aug 08 '22 at 08:36
  • @Hydrogen https://cs.stackexchange.com/a/153472/152730 – Tom Huntington Aug 08 '22 at 09:04