In this section, we will build an iterator together with a range class, which enables us to iterate over a string with unknown length, without finding the end position in advance.
- First, as always, we need to include headers:
#include <iostream>
- The iterator sentinel is a very central element of this section. Surprisingly, its class definition can stay completely empty:
class cstring_iterator_sentinel {};
- Now we implement the iterator. It will contain a string pointer, which is the container we iterate over:
class cstring_iterator {
const char *s {nullptr};
- The constructor just initializes the internal string pointer to whatever string the user provides. Let's make the constructor explicit in order to prevent accidental implicit conversions from strings to string iterators:
public:
explicit cstring_iterator(const char *str)
: s{str}
{}
- When dereferencing the iterator at some point, it will just return the character value at this position:
char operator*() const { return *s; }
- Incrementing the iterator just increments the position in the string:
cstring_iterator& operator++() {
++s;
return *this;
}
- This is the interesting part. We implement the != operator for comparison, as it is used by STL algorithms and the range-based for loop. However, this time, we do not implement it for the comparison of iterators with other iterators, but for comparing iterators with sentinels. When we compare an iterator with another iterator we can only check if their internal string pointers both point to the same address, which is somewhat limiting. By comparing against an empty sentinel object, we can perform a completely different semantic–we check if the character our iterator points to is a terminating '' character because this represents the end of the string!
bool operator!=(const cstring_iterator_sentinel) const {
return s != nullptr && *s != '';
}
};
- In order to use this in a range-based for loop, we need a range class around it, which emits the begin and end iterators:
class cstring_range {
const char *s {nullptr};
- The only thing the user needs to provide during instantiation is the string that will be iterated over:
public:
cstring_range(const char *str)
: s{str}
{}
- We return a normal cstring_iterator from the begin() function, which points to the beginning of the string. From the end() function, we just return the sentinel type. Note that without the sentinel type, we would also return an iterator, but from where should we know the end of the string if we didn't search for it in advance?
cstring_iterator begin() const {
return cstring_iterator{s};
}
cstring_iterator_sentinel end() const {
return {};
}
};
- That's it. We can immediately use it. Strings that come from the user are one example of an input we cannot know the length of in advance. In order to force the user to give some input, we will abort the program if the user did not provide at least one parameter when launching the program in the shell:
int main(int argc, char *argv[])
{
if (argc < 2) {
std::cout << "Please provide one parameter.n";
return 1;
}
- If the program is still being executed up to this point, then we know that argv[1] contains some user string:
for (char c : cstring_range(argv[1])) {
std::cout << c;
}
std::cout << 'n';
}
- Compiling and running the program yields the following output:
$ ./main "abcdef"
abcdef
That the loop prints what we just entered is not a surprise, as this is just quite a micro-example for the implementation of a sentinel-based iterator range. This iteration termination method will help you in implementing your own iterators wherever you run into a situation where the comparison with an end position approach is not helpful.