2

I want to create a class with a loaded file and implement an iterator for given class to iterate over it with range iterator

Here is my class

class Csv
{

  public:
    explicit Csv(std::string filepath);
    ~Csv();
    void load_new_file(std::string filepath) {}

  private:
    std::ifstream file;
};

Here is a behaviour I want to implement

Csv csv("path");
for (auto line : csv ){
    std::cout<<line<<std::endl;
}

How can I do this?

Hubert
  • 142
  • 1
  • 12
  • 4
    Your `begin()` could return a [`std::istream_iterator`](https://en.cppreference.com/w/cpp/iterator/istream_iterator). You'll have to figure `end()` out - but it's similar :-) – Ted Lyngmo Jul 03 '20 at 13:18
  • 1
    Does this answer your question? [How to make the for each loop function in C++ work with a custom class](https://stackoverflow.com/questions/16504062/how-to-make-the-for-each-loop-function-in-c-work-with-a-custom-class) – Simon Kraemer Jul 03 '20 at 13:18
  • 1
    @TedLyngmo, sounds like cheating. :) – Evg Jul 03 '20 at 13:19
  • Additional information: https://en.cppreference.com/w/cpp/language/range-for – Simon Kraemer Jul 03 '20 at 13:19
  • 1
    @SimonKraemer thank you for your input, but no. My problem is I don't know how to implement `begin()` and `end()` for ifstream, I will look into `std::istream_iterator` – Hubert Jul 03 '20 at 13:21
  • 1
    @Hubert, is it some kind of assignment or you need it for some real project? – Evg Jul 03 '20 at 13:22
  • @Evg real project – Hubert Jul 03 '20 at 13:23
  • 2
    And what is semantics of iteration? Line by line or comma-separated-values as class name suggests? – Evg Jul 03 '20 at 13:24
  • 1
    @Evg line by line, I will handle commas and other csv separators using boost::tokenizer when needed on each line – Hubert Jul 03 '20 at 13:25
  • 2
    @Hubert Here is an overview of the different kind of iterators and the requirements/specifics they are supposed to fulfil/have: https://en.cppreference.com/w/cpp/iterator – Simon Kraemer Jul 03 '20 at 13:26
  • 4
    And if you don't need some special thing in the boost tokenizer, you also have the [`std::regex_token_iterator`](https://en.cppreference.com/w/cpp/regex/regex_token_iterator) that is included in standard c++. – Ted Lyngmo Jul 03 '20 at 13:29
  • 1
    And "abc def" without a line break should produce one line or two lines? – Evg Jul 03 '20 at 13:35
  • @Evg one line, the iteration over `Csv class` should behave like `getline(std::ifstream, std::string)` – Hubert Jul 03 '20 at 13:37
  • 1
    @TedLyngmo Too bad `istream_iterator` is not bidirectional - would be a rather elegant solution. – Simon Kraemer Jul 03 '20 at 13:46
  • @SimonKraemer Yes, that's true. I can't imagine it being very hard to write one especially made for `fstream`s if that's needed though.Your link to the list of iterator categories may help OP to select which category the iterator should be in. – Ted Lyngmo Jul 03 '20 at 13:57

3 Answers3

3

Minimal example that uses std::getline:

class End_iterator {};

class Iterator {
public:
    Iterator(std::ifstream& file) : file_{file} {
        next();
    }

    const std::string& operator*() const {
        return str_;
    }

    Iterator& operator++() {
        next();
        return *this;
    }

    bool operator!=(End_iterator) const {
        return !!file_;
    }

private:
    void next() {
        std::getline(file_, str_);
    }

    std::ifstream& file_;
    std::string str_;
};

class Csv {
public:
    explicit Csv(std::string filepath) {
        file_.open(filepath);
    }

    auto begin() {
        return Iterator{file_};
    }

    auto end() {
        return End_iterator{};
    }

private:
    std::ifstream file_;
};

C++17 allows us to use begin and end iterators of distinct types in a range-based for loop. It is easier (and somewhat more elegant) to make End_iterator a distinct type (a sentinel).

Evg
  • 25,259
  • 5
  • 41
  • 83
2

This is a simple example of a custom iterator that satisfies the LegacyInputIterator requirements:

class LineFileIterator
{
public:
    using value_type = std::string;
    using difference_type = std::ptrdiff_t;
    using reference = const std::string&;
    using pointer = const std::string*;
    using iterator_category = std::input_iterator_tag;
    LineFileIterator(): file(nullptr) {}
    explicit LineFileIterator(std::ifstream& f): file(&f) {}
    
    LineFileIterator& operator++()
    {
        read();
        return *this;
    }
    LineFileIterator operator++(int)
    {
        LineFileIterator tmp = *this;
        read();
        return tmp;
    }
    
    const std::string& operator*() const
    {
        return currentLine;
    }
    
    friend bool operator!=(const LineFileIterator& a, const LineFileIterator& b)
    {
        return not a.equal(b);
    }
    
    friend bool operator==(const LineFileIterator& a, const LineFileIterator& b)
    {
        return a.equal(b);
    }
private:
    bool equal(const LineFileIterator& it) const
    {
        return file == it.file;
    }
    
    void read()
    {
        if(not std::getline(*file, currentLine))
        {
            file = nullptr;
        }
    }

    std::ifstream* file;
    std::string currentLine;
};

The begin method should return LineFileIterator{file}, and end method should return a default constructed LineFileIterator.

Edit: This implementation works since C++11.

Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
1

In c++ 17 the range based for has been implemented like this

{
  auto && __range = range_expression ;
  auto __begin = begin_expr;
  auto __end = end_expr;
  for (;__begin != __end; ++__begin) {
    range_declaration = *__begin;
    loop_statement
  }
}

You may check here

If you want to make you class suitable for using the range based for loop, you should define 5 functions:

  • begin()
  • end()
  • operator *()
  • void operator ++()
  • bool operator != (...& other)

And because you want that your class has an iterator and NOT is an iterator, we will simply define an internal iterator to the underlying data structure and do all operations with this iterator.

That's a difference to the other answers, where the whole class is an iterator.

This makes life real easy.

Reading the complete file is a one-liner.

By the way, parsing the file is also a one liner by using std::regex_token_iterator. There is no need for boost or something. Reading a file and splitting it into Tokens is als a typical one-liner.

Please see the simple program below:

#include <iostream>
#include <sstream>
#include <vector>
#include <fstream>
#include <regex>
#include <iterator>
#include <algorithm>

using Lines = std::vector<std::string>;
std::regex re{ "," };

class Csv {
    // Here we staore all lines of the file
    Lines lines{};

    // Local iterator to this lines
    Lines::iterator lineIterator{};

    // Function to read all lines of the file
    void readData(const std::string& fileName);

public:
    // Simple constructor
    explicit Csv(std::string fileName) { readData(fileName); }
    ~Csv() {};

    // Iterators to access the lines data
    Lines::iterator begin() { lineIterator = lines.begin(); return lineIterator;  };
    Lines::iterator end() { lineIterator = lines.end(); return lineIterator; };
    std::string& operator *() { return *lineIterator; }
    void operator ++() { ++lineIterator; }
    bool operator != (const Lines::iterator& other) { return other != lineIterator;  }
};

void Csv::readData(const std::string& fileName) {

    // Open File and check, if it could be opened
    if (std::ifstream fileStream{ fileName }; fileStream) {

        // Clear old existing data
        lines.clear(); 

        // Read all lines
        for (std::string line{}; std::getline(fileStream, line); lines.push_back(line))
            ;
    }
    else {
        std::cerr << "\n*** Error: Could not open file '" << fileName << "'\n";
    }
}

int main() {

    // Open file and read all lines
    Csv csv("r:\\text.txt");

    // Range based for for iterating over the lines in the file
    for (const std::string& line : csv) {
        std::cout << "\n\n" << line << "   ->\n";

        std::copy(std::sregex_token_iterator(line.begin(), line.end(), re, -1), {}, std::ostream_iterator<std::string>(std::cout, "\t"));
    }

    return 0;
}
A M
  • 14,694
  • 5
  • 19
  • 44
  • I don't understand what `lineIterator = lines.begin();` in `begin()` and `end()` is for. Also note that your member functions `operator*()`, `operator++()`, and `operator!=()` are not called at all. – Evg Jul 03 '20 at 15:11