8

I like the idea of the lazy ranges you can make with std::views::iota but was surprised to see that iota is currently the only thing like it in the standard; it is the only "range factory" besides views::single and views::empty. There is not currently, for example, the equivalent of std::generate as a range factory.

I note however it is trivial to implement the semantics of generate by using a transform view on iota and just ignoring the value iota passes to transform i.e.

#include <iostream>
#include <ranges>
#include <random>

template<typename F>
auto generate1(const F& func) {
    return std::views::iota(0) | std::views::transform([&func](int) {return func(); });
}

std::random_device dev;
std::mt19937 rng(dev());

int main() {

    auto d6 = []() {
        static std::uniform_int_distribution<> dist(1, 6);
        return dist(rng);
    };

    for (int v : generate1(d6) | std::views::take(10)) {
        std::cout << v << ' ';
    }
    std::cout << '\n';
}

My questions is what would be "the real way" to implement something like this? To make a range view object that is pipeable that does not just use iota.

I tried inheriting from ranges::view_interface -- no idea if this is the correct approach -- and just having it return a dummy iterator that calls a generator function but my code doesn't work because of the part where it needs to pipe the range view to std::views::take in order to not cause an infinite loop. The object I define here does not end up being pipeable.

#include <iostream>
#include <ranges>
#include <random>

template<typename F>
class generate2 : public std::ranges::view_interface<generate2<F>>
{
    using value_type = decltype(std::declval<F>()());

    class iterator {
        const F* gen_func_;
    public:
        iterator(const F* f) : gen_func_(f)
        {}

        value_type operator*() const {
            return (*gen_func_)();
        }

        bool operator!=(const iterator&) {
            return true;
        }

        iterator& operator++() {
            return *this;
        }
    };

    F generator_func_;

public:

    generate2(const F& f) : generator_func_(f) {
    }

    iterator begin()  {
        return iterator(&generator_func_);
    }

    iterator end()  {
        return iterator(nullptr);
    }
};

std::random_device dev;
std::mt19937 rng(dev());

int main() {

    auto d6 = []() {
        static std::uniform_int_distribution<> dist(1, 6);
        return dist(rng);
    };

    // the following doesnt compile because of the pipe...
    for (int v : generate2(d6) | std::views::take(10)) { 
        std::cout << v << ' ';
    }
    std::cout << '\n';
}
jwezorek
  • 8,592
  • 1
  • 29
  • 46
  • It is not your responsibility to make it pipeable. You only need to make your type satisfy the `range` concept and `view::take` will automatically support it. Notably, the `range` concept requires the iterator type to define an associated `difference_type` (usually in the form of a member typedef), and the `operator*` be stable (repeated invocations produce the same result). – cpplearner Feb 16 '22 at 19:41
  • how does it enforce the stability criterion? because my current attempt clearly violates this property. – jwezorek Feb 16 '22 at 19:43
  • @jwezorek: It doesn't enforce anything. It requires *you* to *conform* to the requirement. If you don't, then your program will exhibit undefined behavior. – Nicol Bolas Feb 16 '22 at 20:45
  • well what i have currently doesn't compile by "enforcing" I meant what is the concept I am not satisfying that is causing it to not compile. Although if this is what was happenign Im sure it would tell me about a concept in the error message so idk. – jwezorek Feb 17 '22 at 02:21

1 Answers1

7

The reason why generate2 cannot work is that it does not model the range concept, that is, the type returned by its begin() does not model input_iterator, because input_iterator requires difference_type and value_type to exist and i++ is a valid expression.

In addition, your iterator does not satisfy sentinel_for<iterator>, which means that it cannot serve as its own sentinel, because sentinel_for requires semiregular which requires default_initializable, so you also need to add default constructors for it.

You also need to rewrite bool operator!=(...) to bool operator==(...) const since operator!= does not reverse synthesize operator==. But it's easier to just use default_sentinel_t as sentinel in your case.

if you add them to iterator you will find the code will be well-formed:

class iterator {
 public:
  using value_type = decltype(std::declval<F>()());
  using difference_type = std::ptrdiff_t;
  iterator() = default;
  void operator++(int);
  bool operator==(const iterator&) const {
    return false;
  }
  // ...
};

However, the operator*() of iterator does not meet the requirements of equality-preserving, that is to say, the results obtained by the two calls before and after are not equal, which means that this will be undefined behavior.

You can refer to the implementation of ranges::istream_view to use a member variable to cache each generated result, then you only need to return the cached value each time iterator::operator*() is called.

template<typename F>
class generate2 : public std::ranges::view_interface<generate2<F>> {
 public:
  auto begin() {
    value_ = generator_func_();
    return iterator{*this};
  }

  std::default_sentinel_t end() const noexcept { return std::default_sentinel; }

  class iterator {
   public:
   //...
    value_type operator*() const {
      return parent_->value_;
    }
   private:
    generate2* parent_;
  };

 private:
   F generator_func_;
   std::remove_cvref_t<std::invoke_result_t<F&>> value_; 
};
康桓瑋
  • 33,481
  • 5
  • 40
  • 90
  • With all of these changes it still seems to be not working: https://godbolt.org/z/zxWr3rP5z – jwezorek Feb 17 '22 at 16:36
  • @jwezorek. Your `iterator::operator++()` needs to return `iterator&`. Also, since [P2325](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2325r3.html) was not implemented before gcc-12, which makes `generate2` have to be `default_initializable`, so you also need to provide the default constructor for `generate2` like `generate2() = default`. [Demo](https://godbolt.org/z/T8je5xoP6) – 康桓瑋 Feb 17 '22 at 17:24
  • Thanks. What is strange to me about this is how bad the error messages were in both Visual Studio and GCC (I didnt try Clang). I thought one of the big advantages of concepts was supposed to be more meaningful error spew? – jwezorek Feb 18 '22 at 01:15
  • 2
    In fact, for the form of `r | std::views::xxx`, both gcc and msvc only check the validity of the expression, so the error message is not intuitive to the user. I usually convert it to `std::ranges::xxx_view(r)` form for debugging, which can relatively clearly tell me which specific constraint is not satisfied. Using concepts to provide more meaningful error messages is an evolving vision, but the reality is that sometimes the concept error messages are not very helpful. – 康桓瑋 Feb 18 '22 at 01:48