【问题标题】:How to lazily generate a finished sequence of items and iterate over it如何懒惰地生成完成的项目序列并对其进行迭代
【发布时间】:2018-09-04 08:56:54
【问题描述】:

我觉得这个问题一定被问过并解决了很多次,因为在我看来这似乎是一个很笼统的场景,但我找不到任何可以让我找到解决方案的方向。

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

本质上,基本思想是拥有类似于 Python 的生成器的东西,其中一个对象产生值直到它没有更多的产生,然后引发 StopIteration 异常以通知外部循环序列已完成。

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

对于生成序列的对象,我想我会定义一个基本的 Generator 类,然后扩展它以提供特定的行为(例如,从一组范围或固定值列表中获取值等)。所有Generaors 都会在每次调用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; }
};

但是,对于迭代器部分,我被卡住了。 我研究了我能想到的“迭代器”、“生成器”和同义词的任何组合,但我发现的只是考虑了生成器函数具有无限数量的值的情况(例如参见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 定义迭代器。

【问题讨论】:

  • 只为您的迭代器设置一个标记状态,表示“结束”,并编写您的比较运算符,以便 i == end 只有在 i 达到其终端条件时才为真。这可以像添加bool isAtEnd 成员一样简单。不要使用异常,将 Python 约定导入 C++
  • 是的,我觉得使用异常是不好的做法,但是来自 Python 的我选择了我知道的解决方案。问题是,IMO 异常清楚地将“序列中还有另一个值”的情况与“序列结束”分开。我将如何通知调用generator() 的人该实例实际上无一例外地运行了整个序列?我不能假设我总是有一个值要返回......(写这个我想std::optional&lt;T&gt;作为operator()的返回值......会更好吗?)
  • 我认为您试图对 Python 建模过于紧密,这就是为什么您现在无法将 Python 生成器与 C++ 迭代器匹配。 C++ 中的迭代器在异常之前不会调用,您可以比较它们。您的“结束”迭代器是 m_next_val == m_stop 的迭代器。您还需要编写一个相等方法来检查这一点。
  • 如果你想生成无限序列,你需要迭代器接口吗?当你得到一个迭代器时,你接下来会用它做什么? (即用例)
  • @Hitobat 与其他语言相同,例如:en.cppreference.com/w/cpp/algorithm/copy_n

标签: c++ iterator


【解决方案1】:

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

【讨论】:

  • 您不需要startstopstep 成员,因为这些值是模板参数。您也不需要比较 stepstop,因为它们只能在不同的实例中有所不同。
  • @molbdnilo 我同意。事实上,该迭代器在开始有用之前需要额外的增强。这只是一个示例,以避免复杂和冗长的解释。
【解决方案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-&gt; 等. 等等)

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

【讨论】:

  • 谢谢,这是迄今为止我看到的最接近我最初需要的东西。虽然其他答案确实为更简单的子问题提供了解决方案,但我宁愿有一个适用于任何仿函数的通用解决方案。我需要一些时间来理解你的代码,但我相信这正是我所需要的
  • 请注意,如果您的生成器函数更自然地编写为抛出异常或返回一些标记值,您可以轻松编写一个适配器函数将其转换为所需的std::optional 形式。
  • 关于迭代器特征,YSC 将它们显示为类型 int。使用泛型类型 T,difference_type 会是什么?我还应该使用int吗?或者std::ptrdiff_t,就像我在一些例子中看到的那样?另外,我是否正确理解在我的情况下input_iterator_tag 是正确的选择? (前向迭代器要求值是可修改的,这不是我的情况)
  • 正确,它是一个输入迭代器(函数可能是有状态的,所以我们只能假设单遍)。对于差异类型,intsize_t 都可以。大多数情况下我没有写它们,因为 value_type、reference_type 和 pointer_type 需要删除引用才能完全安全(一个函数可以返回 optional> 到外部存储的持久对象)。
  • 我不确定我是否得到了关于删除引用的部分(模板编程对我来说几乎是全新的),但我计划至少只使用简单类型的类(很可能只是浮点数)现在,所以我相信我可以忽略那部分。根据不同类型,是否需要签名? (如果是这样,size_t 将不起作用)
【解决方案3】:

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

【讨论】:

  • 我读到它已经在 C++17 中被弃用了(虽然我现在可以忍受)
【解决方案4】:

您描述的用例(范围的串联等)可能证明对库的依赖是合理的,所以这里有一个基于 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";

请注意,上面的 sn-ps 使用 -std=c++17 编译。该库只是标题。

【讨论】:

  • 这看起来很棒,我希望它已经包含在标准中,但遗憾的是目前这对我来说遥不可及。此外,我需要能够在迭代浮点数时更改步骤,我认为这不支持(尽管我没有深入研究库)
猜你喜欢
  • 2010-09-26
  • 2016-03-01
  • 2011-12-31
  • 1970-01-01
  • 2019-06-20
  • 2018-04-17
  • 1970-01-01
  • 1970-01-01
  • 2021-06-09
相关资源
最近更新 更多