【问题标题】:C++ custom lazy iteratorC++ 自定义惰性迭代器
【发布时间】:2019-06-09 19:27:00
【问题描述】:

我有一个比较简单的文本文件解析器。我解析的文本被分割成由{ block data } 表示的块。

我的解析器有一个string read() 函数,它可以取回标记,因此在上面的示例中,第一个标记是{,然后是block,然后是data,然后是}

为了减少重复,我想编写一个类似于生成器的迭代器,它允许我编写类似于以下 JavaScript 代码的内容:

* readBlock() {
    this.read(); // {

    let token = this.read();

    while (token !== '}') {
        yield token;

        token = this.read();
    }
}

这反过来又允许我使用简单的 for-of 语法:

for (let token of parser.readBlock()) {
    // block
    // data
}

对于 C++,我想要类似的东西:

for (string token : reader.read_block())
{
    // block
    // data
}

我四处搜索,看看是否可以使用迭代器来完成,但我不知道我是否可以拥有像这样没有定义开始或结束的惰性迭代器。也就是说,它的开始是阅读器的当前位置(字符向量中的整数偏移量),它的结束是找到标记} 的时间。 我不需要构造任意迭代器,或者反向迭代,或者查看两个迭代器是否相等,因为它纯粹是为了减少线性迭代的重复性。

目前每次想读一个block,都需要重写如下:

stream.skip(); // {
while ((token = stream.read()) != "}")
{
    // block
    // data
}

这变得非常混乱,尤其是当我在块内有块时。为了支持块内的块,迭代器必须都引用同一个读取器的偏移量,这样内部块将提前偏移量,而外部块将从该高级偏移量重新开始迭代(内部完成后)。

这可以在 C++ 中实现吗?

【问题讨论】:

  • 是的,很有可能。您所需要的只是为您的文件解析器制作一个类似迭代器的包装器。 begin()end()operator++operator*operator== 应该足够了。
  • 如果我不知道什么时候到达终点,那将如何运作?
  • 所有迭代不都是这样工作的吗?你循环直到你到达终点,然后你停下来。因此,当您的解析器到达 } 时,这将相当于 end() 迭代器。

标签: c++ iterator


【解决方案1】:

为了在 for-range 循环中可用,类必须具有返回迭代器的成员函数 begin() 和 end()。

什么是迭代器?满足一组要求的任何对象。有几种迭代器,具体取决于您允许哪些操作。我建议实现一个输入迭代器,这是最简单的:https://en.cppreference.com/w/cpp/named_req/InputIterator

class Stream
{
public:
    std::string read() { /**/ }
    bool valid() const { /* return true while more tokens are available */ }
};

class FileParser
{
    std::string current_;
    Stream* stream_;
public:
    class iterator
    {
        FileParser* obj_;
    public:
        using value_type = std::string;
        using reference = const std::string&;
        using pointer = const std::string*;
        using iterator_category = std::input_iterator_tag;
        iterator(FileParser* obj=nullptr): obj_ {obj} {}
        reference operator*() const { return obj_->current_; }
        iterator& operator++() { increment(); return *this; }
        iterator operator++(int) { increment(); return *this; }
        bool operator==(iterator rhs) const { return obj_ == rhs.obj_; }
        bool operator!=(iterator rhs) const { return !(rhs==*this); }
    protected:
        void increment()
        {
            obj_->next();
            if (!obj_->valid())
                obj_ = nullptr;
        }
    };


    FileParser(Stream& stream): stream_ {&stream} {};
    iterator begin() { return iterator{this}; }
    iterator end() { return iterator{}; }
    void next() { current_ = stream_->read(); }
    bool valid() const { return stream_->valid(); }
};

因此,您的文件结尾迭代器由指向任何对象的迭代器表示。

那么你可以这样使用它:

int main()
{
    Stream s; // Initialize it as needed
    FileParser parser {s};
    for (const std::string& token: parser)
    {
        std::cout << token << std::endl;
    }
}

【讨论】:

  • 谢谢!这允许我在 for 循环中通过一个简单的 stream.read_block() 调用添加我想要的确切签名:)
  • 主要是出于好奇,因为这段代码不需要特别快,但是你知道它与上面手动编写块读取代码相比是否会显着降低性能吗?
  • 只是跟进,从我自己的测试用例来看,使用迭代器似乎要慢 20% 左右。或许为了更好的语法而做出了太多的权衡。还是很酷。
  • 嗯,我没想到会这么慢。你做了什么测试(你使用了哪些编译选项)?知道这背后的原因会很有趣。
猜你喜欢
  • 2011-01-16
  • 2015-11-19
  • 2010-10-24
  • 1970-01-01
  • 2012-09-20
  • 2018-05-13
  • 2015-02-04
  • 1970-01-01
相关资源
最近更新 更多