How to lazily generate a finished sequence of items and iterate over it

Viewed 1088

I feel like this question must have been asked and solved many times, because this seems to me a quite generic scenario, but I could not find anything that pointed me in the direction of a solution.

I'm trying to implement a generic iterable Generator object that produces a sequence of numbers up until a certain termination condition is met, signaling that such condition has been reached in order to stop the iteration.

The basic idea is, essentially, to have something similar to Python's generators where an object yields values until it has no more to yield and then a StopIteration exception is raised to inform the outside loop that the sequence is finished.

From what I understand, the problem splits into creating the sequence-generating object an then obtaining an iterator over it.

For the sequence-generating object, I thought I'd define a base Generator class which is then extended to provide specific behaviours (e.g., get values from a set of ranges, or from a list of fixed values, etc). All Generaors produce a new value at each call of operator() or throw a ValuesFinishedException if the generator ran to the end of the sequence. I implemented this as such (I show the single-range subclass as example, but I need to be able to model more types of sequences):

struct ValuesFinishedException : public std::exception { };

template <typename T>
class Generator
{
public:
    Generator() { };
    ~Generator() { };
    virtual T operator()() = 0; // return the new number or raise a ValuesFinishedException
};

template <typename T>
class RangeGenerator : public Generator<T>
{
private:
    T m_start;
    T m_stop;
    T m_step;

    T m_next_val;

public:
    RangeGenerator(T start, T stop, T step) :
        m_start(start),
        m_stop(stop),
        m_step(step),
        m_next_val(start)
    { }

    T operator()() override
    {
        if (m_next_val >= m_stop)
            throw ValuesFinishedException();

        T retval = m_next_val;
        m_next_val += m_step;
        return retval;
    }

    void setStep(T step) { m_step = step; }
    T step() { return m_step; }
};

For the iterator part, though, I'm stuck. I have researched any combination I could think of of "Iterator", "Generator" and synonyms, but all I find only considers the case where a generator function has an unlimited number of values (see for example boost's generator_iterator). I thought about writing a Generator::iterator class myself, but I only found examples of trivial iterators (linked lists, array reimplementations) where end is well-defined. I don't know in advance when the end will be reached, I only know that if the generator I'm iterating over raises the exception, I need to set the iterator's current value to "end()", but I don't know how to represent it.

Edit: Adding the intended use-case

The reason for this class is to have a flexible sequence object I can loop over:

RangeGenerator gen(0.25f, 95.3f, 1.2f);
for(auto v : gen)
{
    // do something with v
}

The example of the range is just the simplest one. I will have at least three actual use-cases:

  • simple range (with variable step)
  • concatenation of multiple ranges
  • sequence of constant values stored in a vector

For each of these I'm planning on having a Generator subclass, with the iterator defined for the abstract Generator.

4 Answers

You should use a C++ idiom: the forward iterator. This let you use C++ syntactic sugar and support the standard library. Here is a minimal example:

template<int tstart, int tstop, int tstep = 1>
class Range {
public:
    class iterator {
        int start;
        int stop;
        int step;
        int current;
    public:
        iterator(int start, int stop, int step = 0, int current = tstart) : start(start), stop(stop), step(step == 0 ? (start < stop ? 1 : -1) : step), current(current) {}
        iterator& operator++() {current += step; return *this;}
        iterator operator++(int) {iterator retval = *this; ++(*this); return retval;}
        bool operator==(iterator other) const {return std::tie(current, step, stop) == std::tie(other.current, other.step, other.stop);}
        bool operator!=(iterator other) const {return !(*this == other);}
        long operator*() {return current;}
        // iterator traits
        using difference_type = int;
        using value_type = int;
        using pointer = const int*;
        using reference = const int&;
        using iterator_category = std::forward_iterator_tag;
    };
    iterator begin() {return iterator{tstart, tstop, tstep};}
    iterator end() {return iterator{tstart, tstop, tstep, tstop};}
};

It can be used with the C++98 way:

using range = Range<0, 10, 2>;
auto r = range{};
for (range::iterator it = r.begin() ; it != r.end() ; ++it) {
    std::cout << *it << '\n';
}

Or with the new range loop:

for (auto n : Range<0, 10, 2>{}) {
    std::cout << n << '\n';
}

In cunjunction with the stl:

std::copy(std::begin(r), std::end(r), std::back_inserter(v));

Demo: http://coliru.stacked-crooked.com/a/35ad4ce16428e65d

The range based for loop is all about the iterator implementing begin(), end() and operator++.

So the generator must implement them.

template<typename T>
struct generator {
    T first;
    T last;

    struct iterator {
        using iterator_category = std::input_iterator_tag;
        using value_type = T;
        using difference_type = std::ptrdiff_t;
        using pointer = T *;
        using reference = T &;
        T value;

        iterator(T &value) : value(value) {}

        iterator &operator++() {
            ++value;
            return *this;
        }
        iterator operator++(int) = delete;

        bool operator==(const iterator &rhs) { return value == rhs.value; }
        bool operator!=(const iterator &rhs) { return !(*this == rhs); }
        const reference operator *() { return value; }
        const pointer operator->() const { return std::addressof(value); }
    };

    iterator begin() { return iterator(first); }
    iterator end() { return iterator(last); }
};

Then add a function which instantiates the generator and you are done

template<typename T>
generator<T> range(T start, T end) {
    return generator<T>{ start, end };
}

for (auto i : range(0, 10))
{
}

If you want the generic generator you originally asked for (rather than the simpler use-cases added later), it is possible to set up something like this:

template <typename T>
struct Generator {
    Generator() {}
    explicit Generator(std::function<std::optional<T>()> f_) : f(f_), v(f()) {}

    Generator(Generator<T> const &) = default;
    Generator(Generator<T> &&) = default;
    Generator<T>& operator=(Generator<T> const &) = default;
    Generator<T>& operator=(Generator<T> &&) = default;

    bool operator==(Generator<T> const &rhs) {
        return (!v) && (!rhs.v); // only compare equal if both at end
    }
    bool operator!=(Generator<T> const &rhs) { return !(*this == rhs); }

    Generator<T>& operator++() {
        v = f();
        return *this;
    }
    Generator<T> operator++(int) {
        auto tmp = *this;
        ++*this;
        return tmp;
    }

    // throw `std::bad_optional_access` if you try to dereference an end iterator
    T const& operator*() const {
        return v.value();
    }

private:
    std::function<std::optional<T>()> f;
    std::optional<T> v;
};

if you have C++17 (if you don't, use Boost or just track validity manually). The begin/end functions needed to use this nicely look something like

template <typename T>
Generator<T> generate_begin(std::function<std::optional<T>()> f) { return Generator<T>(f); }
template <typename T>
Generator<T> generate_end(std::function<std::optional<T>()>) { return Generator<T>(); }

Now for a suitable function foo you can use this like a normal input operator:

auto sum = std::accumulate(generate_begin(foo), generate_end(foo), 0);

I omitted the iterator traits which should be defined in Generator as they are in YSC's answer- they should be something like the below (and operator* should return reference, and you should add operator->, etc. etc.)

    // iterator traits
    using difference_type = int;
    using value_type = T;
    using pointer = const T*;
    using reference = const T&;
    using iterator_category = std::input_iterator_tag;

The use case you describe (concatenation of ranges etc.) might justify a dependency on a library, so here is a solution based on range-v3, on its way into C++20. You can easily iterate through integral values, here from 0 to 10 with a step size of 2,

#include <range/v3/all.hpp>

using namespace ranges;

for (auto i : view::ints(0, 11) | view::stride(2))
   std::cout << i << "\n";

or implement a similar loop with floating point values (note that [from, to] is a closed range here, and the third argument denotes the number of steps)

for (auto f : view::linear_distribute(1.25f, 2.5f, 10))
   std::cout << f << "\n";

and when it comes to concatenation, the library begins to shine:

const std::vector world{32, 119, 111, 114, 108, 100};

for (auto i : view::concat("hello", world))
   std::cout << char(i);

std::cout << "\n";

Note that the above snippets compile with -std=c++17. The library is header only.

Related