Custom Iterator
Intro
In C++, to read each line of an open file, the idiomatic way is:
std::ifstream ifs("hello.txt");
for (std::string line; std::getline(ifs, line);) {
// do something with `line`
}
It’s simple and straightforward, but doesn’t compose well with the range-based algorithms of the standard library. That is because a file is not a range, although conceptually we can treat it as a range of lines. For example, in Python:
for line in open("hello.txt"):
# do something with `line`
We can achieve the same thing in C++ with custom iterators.
Writing conforming iterators, unfortunately, involves some boilerplate code. This post provides some code templates to help with that (assuming C++20).
Input Iterator
Reusing our intro example, a line-iterator:
// Iterate lines through a std::istream by std::getline.
class line_iterator {
public:
using difference_type = int;
using value_type = std::string;
using pointer = value_type*;
using reference = value_type&;
using iterator_category = std::input_iterator_tag;
static struct sentinel_type {} sentinel;
private:
std::istream* is_;
value_type value_;
// business logic for getting the next value
void next() {
std::getline(*is_, value_);
}
// business logic for checking whether there is no more value
bool done() const {
return is_->eof();
}
public:
line_iterator(std::istream& is) : is_(&is) { this->next(); }
const value_type& operator*() const { return value_; }
line_iterator& operator++() {
this->next();
return *this;
}
line_iterator operator++(int) {
auto old = *this;
++(*this);
return old;
}
bool operator==(sentinel_type) const { return this->done(); }
};
And the accompanying range:
class iter_line {
private:
std::istream& is_;
public:
iter_line(std::istream& is) : is_(is) {}
auto begin() { return line_iterator(is_); }
auto end() { return line_iterator::sentinel; }
};
So we can write:
std::ifstream ifs("hello.txt");
for (auto& line: iter_line(ifs)) {
// do something with `line`
}
Note that the following checks would pass:
static_assert(std::input_iterator<line_iterator>);
static_assert(std::ranges::input_range<iter_line>);
static_assert(
std::is_same_v<std::iterator_traits<line_iterator>::iterator_category,
std::input_iterator_tag>);
Output Iterator
Output iterators are very different from input iterators. In fact, they are barely iterators; They are more like adaptors that wrap some function taking output (a “sink”) in a iterator class.
Let’s write an output iterator that adds prefix and postfix (the std::ostream_iterator
adds postfix only):
// Prefix and postfix value output to std::ostream using operator<<.
class ostream_iterator_2 {
public:
using difference_type = int;
using iterator_category = std::output_iterator_tag;
private:
std::ostream* os_;
std::string prefix_;
std::string postfix_;
// business logic for outputting a value
template <class T>
void output(const T& x) {
(*os_) << prefix_ << x << postfix_;
}
public:
ostream_iterator_2(std::ostream& os, std::string prefix, std::string postfix)
: os_(&os), prefix_(std::move(prefix)), postfix_(std::move(postfix)) {}
ostream_iterator_2& operator*() { return *this; }
ostream_iterator_2& operator++() { return *this; }
ostream_iterator_2& operator++(int) { return *this; }
template <class T>
ostream_iterator_2& operator=(const T& x) {
output(x);
return *this;
}
};
With line_iterator
and ostream_iterator_2
, we can write:
// prepend filename to each line of the input file, and print to stdout
const char* filename = "input_file.txt";
std::ifstream ifs(filename);
std::ranges::copy(iter_line(ifs), ostream_iterator_2(std::cout, filename + std::string(": "), "\n"));
Output Iterator using std::back_insert_iterator
An alternative to writing ad-hoc output iterators is to reuse std::back_insert_iterator
.
Simply add the following to your class that will accept the output value:
class my_application {
public:
// our payload
using value_type = std::string;
// called to supply a new value
void push_back(const value_type&);
};
The above example can be rewritten as:
// Prefix and postfix value output to std::ostream using operator<<.
class printer {
private:
std::ostream* os_;
std::string prefix_;
std::string postfix_;
// business logic for outputting a value
template <class T>
void output(const T& x) {
(*os_) << prefix_ << x << postfix_;
}
public:
printer(std::ostream& os, std::string prefix, std::string postfix)
: os_(&os), prefix_(std::move(prefix)), postfix_(std::move(postfix)) {}
using value_type = std::string;
void push_back(const value_type& x) {
output(x);
}
};
// prepend filename to each line of the input file, and print to stdout
const char* filename = "input_file.txt";
std::ifstream ifs(filename);
printer p(std::cout, filename + std::string(": "), "\n");
std::ranges::copy(iter_line(ifs), std::back_inserter(p));
Afterword
Using custom iterators can help improve our code quality; getting data is now separated from processing data (algorithms) and can be tested separately.