Regarding the pause-resume data loss in MSVC std::experimental::generator
Asked Answered
A

2

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)

Aneroidograph answered 2/8, 2022 at 11:48 Comment(6)
Why not report the problem to Microsoft?Infielder
@Infielder 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.Aneroidograph
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 elementKnives
This is the expected behavior, which is basically equivalent to this.Ansell
@Knives Can I "bypass" this "feature" somehow?Aneroidograph
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.Tagmeme
G
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

Golem answered 2/8, 2022 at 13:1 Comment(2)
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?Aneroidograph
Thank you so much! This is exactly what I wanted to do!Aneroidograph
H
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

Highsounding answered 3/8, 2022 at 7:36 Comment(10)
This alternative solution looks cool! Could you explain a bit about what the std::moveis doing in the lambda capture? Much appreciated!Aneroidograph
@Aneroidograph 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 itHighsounding
@Aneroidograph also note that you could also leave the std::generator on the stack and accept it by reference to a stateless adaptor function godbolt.org/z/cnP9d5ohaHighsounding
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 behaviourHighsounding
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".Aneroidograph
"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 correctnessHighsounding
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. XDAneroidograph
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 scienceHighsounding
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 🤣Aneroidograph
@Aneroidograph cs.stackexchange.com/a/153472/152730Highsounding

© 2022 - 2024 — McMap. All rights reserved.