Как лениво генерировать готовую последовательность элементов и перебирать ее

я пытаюсь реализовать общий iterable Generator объект, который производит последовательность чисел до тех пор, пока не будет выполнено определенное условие завершения, сигнализируя, что такое условие было достигнуто, чтобы остановить итерацию.

основная идея, по сути, иметь что-то похожее на генераторы Python, где объект дает значения, пока он больше не будет давать, а затем StopIteration exception возникает, чтобы сообщить внешнему циклу, что последовательность завершена.

из того, что я понимаю, проблема разбивается на создание объекта генерации последовательности, а затем получение итератора над ним.

для объекта, генерирующего последовательность, я думал, что определю базу Generator класс, который расширяется, чтобы обеспечить специфическое поведение (например, получение значений из набора диапазонов или из списка фиксированных значений и т. д.). Все!--5-->s производят новое значение при каждом вызове operator() и ValuesFinishedException если генератор работал до конца последовательности. Я реализовал это как таковое (я показываю подкласс с одним диапазоном в качестве примера, но мне нужно иметь возможность моделировать больше типов последовательностей):

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

для части итератора, однако, я застрял. Я исследовал любую комбинацию, которую я мог придумать "Iterator", "Генератор" и синонимы, но все, что я нахожу, учитывает только случай, когда функция генератора имеет неограниченное количество значений (см., например,генератор boost_iterator). Я думал написать Generator::iterator сам класс, но я нашел только примеры тривиальных итераторов (связанных списков, переопределений массива), где end четко определено. Я не знаю заранее, когда будет достигнут конец, я знаю только, что если генератор, который я повторяю, вызывает исключение, мне нужно установите текущее значение итератора в " end ()", но я не знаю, как его представить.

Edit: добавление предполагаемого варианта использования

причиной этого класса является наличие гибкого объекта последовательности, который я могу зациклить:

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

пример диапазона является самым простым. У меня будет по крайней мере три реальных случая использования:

  • простой диапазон (с переменным шагом)
  • объединение нескольких диапазоны
  • последовательность постоянных значений, хранящихся в векторе

для каждого из них я планирую приобрести Generator подкласс, с итератором, определенным для абстрактного Generator.

4 ответов


если вы хотите общий генератор, который вы изначально просили (а не более простые варианты использования, добавленные позже), это и можно создать что-то вроде этого:

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

если у вас есть C++17 (если нет, используйте Boost или просто отслеживайте действительность вручную). Функции begin / end, необходимые для использования этого красиво выглядят примерно так:

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>(); }

теперь для подходящей функции foo вы можете использовать это как нормальный оператор ввода:

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

I опущены признаки итератора, которые должны быть определены в Generator как они в ответе YSC - они должны быть чем-то вроде ниже (и operator* должен возвратить reference, а вы должны добавить operator->, 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;

вы должны использовать идиому C++: прямой итератор. Это позволяет использовать синтаксический сахар C++ и поддерживать стандартную библиотеку. Вот минимальный пример:

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

его можно использовать с путем C++98:

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

или с новым циклом диапазона:

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

в cunjunction с stl:

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

демо:http://coliru.stacked-crooked.com/a/35ad4ce16428e65d


диапазон, основанный на цикле for, - это итератор, реализующий begin (), end () и operator++.

Итак, генератор должен реализовать их.

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

затем добавьте функцию, которая создает экземпляр генератора, и все готово

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

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

вариант использования, который вы описываете (конкатенация диапазонов и т. д.) может оправдать зависимость от библиотеки, поэтому вот решение, основанное на диапазон-v3, на пути в C++20. Вы можете легко перебрать целые значения, здесь от 0 до 10 с шагом 2,

#include <range/v3/all.hpp>

using namespace ranges;

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

или реализовать аналогичный цикл со значениями с плавающей запятой (обратите внимание, что [from, to] здесь является замкнутым диапазоном, а третий аргумент обозначает количество шагов)

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

и когда он доходит до конкатенации, библиотека начинает светиться:

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

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

std::cout << "\n";

обратите внимание, что вышеуказанные фрагменты компилируются с -std=c++17. Библиотека является только заголовком.