如何懒惰地生成完成的项目序列并对其进行迭代

时间:2018-09-04 08:56:55

标签: c++ iterator

我觉得这个问题肯定已经被问过很多次,因为在我看来这是一个非常普通的情况,但是我找不到任何指向解决方案的东西。

我正在尝试实现一个通用的可迭代Generator对象,该对象会生成一个数字序列,直到满足某个终止条件为止为止,这表明已达到该条件以便停止迭代。

从本质上讲,基本思想是类似于Python的生成器,在该生成器中,对象会产生值,直到不再产生值为止,然后引发StopIteration异常以通知外部循环该序列已完成

据我了解,问题分解为创建序列生成对象,然后在其上获得迭代器。

对于序列生成对象,我想我要定义一个基础Generator类,然后将其扩展以提供特定的行为(例如,从一组范围或固定值列表中获取值)等)。所有Generaor都会在每次调用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”,“ Generator”和同义词的任何组合,但是我发现的只是考虑到生成器函数具有无限数量的值的情况(例如,参见boost's generator_iterator)。我曾考虑过自己编写一个Generator::iterator类,但是我只发现了end定义明确的琐碎迭代器(链接列表,数组重新实现)的示例。我不知道何时结束,我只知道如果我要迭代的生成器引发异常,我需要将迭代器的当前值设置为“ end()”,但是我不知道知道如何表示。

编辑:添加预期的用例

此类的原因是要有一个可以循环的灵活序列对象:

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

范围的示例只是最简单的一个。我将至少有三个实际用例:

  • 简单范围(步长可变)
  • 多个范围的串联
  • 向量中存储的常数值的顺序

对于每一个我都计划有一个Generator子类,并为抽象Generator定义迭代器。

4 个答案:

答案 0 :(得分:4)

您应该使用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';
}

与stl连接:

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

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

答案 1 :(得分:2)

基于范围的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))
{
}

答案 2 :(得分:2)

如果您要使用最初要求的通用生成器(而不是稍后添加的更简单的用例),则可以设置以下内容

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或手动跟踪有效性)。很好地使用它的开始/结束功能看起来像

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

我省略了应该在Generator中定义的迭代器特征,因为它们在YSC的答案中-它们应该类似于以下内容(并且operator*应该返回reference,您应该添加operator->,依此类推,等等。

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

答案 3 :(得分:1)

您描述的用例(范围的串联等)可能证明对库的依赖是正当的,因此这里是基于range-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进行编译。该库仅是标题。