'How would you implement a lazy "range factory" for C++20 ranges that just calls a generator function?

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';
}


Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source