Is this an acceptable way to create an iterator?

I like the for-loop range in C ++ and want to use it like this:

#include <bits/stdc++.h>

int main()
{
    for (auto s : LineReader("my-very-big-textfile.txt")) {
        cout << s << endl;
    }
    return 0;
}

      

The goal here is to iterate over some data (without first reading all into the container). In this case, text strings, which are strings in a text file. But usually it can be anything (including the generated data).

Here LineReader returns an iterable "pseudo" container. And for that, the for loop needs iterators from the LineReader object. In C ++, a range is defined in terms of a start and end iterator. But I want to use a for-loop range to iterate over data where the end may not be known at startup (for example, reading a line for a line in a (overly large) text file without skipping it first to find the end.).

So, I define that like this:

Disclaimer: Sample code showing the principle, so I didn't "pester" it with excessive use of std ::, error handling, private / public keywords, etc.

struct ReadLineIterator {
    ifstream ifs;
    string line;

    ReadLineIterator() { }
    ReadLineIterator(string filename) : ifs(filename) { }

    bool operator!=(ReadLineIterator& other) {
        return !ifs.eof();
    }

    ReadLineIterator& operator++() {
        getline(ifs, line, '\n');
        return *this;
    }
    string operator*() {
        return line;
    }
};

struct LineReader
{
    string filename;
    LineReader(const string& filename) : filename(filename) { }

    ReadLineIterator begin()
    {
       return ReadLineIterator(filename);
    }

    ReadLineIterator end() // return a not used dummy iterator since this method must exist
    {
        return ReadLineIterator();
    }
};

      

When I run this it works. But I am skeptical about

bool operator!=(ReadLineIterator& other) {
    return !ifs.eof();
}

      

is the correct way to make this statement detect the end of the sequence. This is because I don't have a suitable end object (the end () method just returns a dummy iterator), and no comparisons are made to it either. Instead, I check if the stream is empty.

But I don't understand how I could do it in any other way? At the moment I'm happy with this way of doing it as it works for me, but it would be great to know if there are better ways to do the same. It would also be nice to know if this works with all (C ++) compilers (I'm using GCC), and if so, that it works with future C ++ standards, where iterators may be handled differently.

+3


source to share


2 answers


I would do it in two parts.

One is a class range

that simply acts as a wrapper for stream iterators:

template <class T>
class istream_range {
    std::istream_iterator<T> b;
    std::istream_iterator<T> e;
public:
    istream_range(std::istream &is)
        : b(std::istream_iterator<T>(is))
        , e(std::istream_iterator<T>())
    {}

    std::istream_iterator<T> begin() { return b; }
    std::istream_iterator<T> end() { return e; }
};

      

So this allows you to use istream_iterator

in a loop for a range:

for (auto const &s : istream_range<foo>(myfile))
    // do something with s

      

B is istream_iterator

used operator>>

to extract items from the specified file, so the second part is just a tiny type that extracts a string:



class line {
    std::string data;
public:
    friend std::istream &operator>>(std::istream &is, line &l) {
        std::getline(is, l.data);
        return is;
    }
    operator std::string() const { return data; }    
};

      

So with that, our loop for

becomes something like:

for (auto const &s : istream_range<line>(myfile))
    // do something with s

      

The obvious advantage of this is the decoupling of the two: we can use istream_range<T>

to process the file T

, for whatever T

, that normal stream extraction does the "right thing" (including a lot of custom extractors that we cannot currently understand).

There are a few more possibilities in the answers to the previous question (including LineInputIterator

, which seems to be a little closer to what you are asking for).

+2


source


The standard templated class std::istream_iterator<T>

acts like an iterator that reads sequential T objects from istream (with operator>>(istream &, T &)

), so all you need is a type T that reads strings from istream:

class line {
    std::string line;
    friend std::istream &operator>>(std::istream &in, line &l) {
        return std::getline(in, l.line);
    }
public:
    operator std::string() const { return line; }
};

      



Now return LineReader

LineReader

.

+1


source







All Articles