Jeffrey D. Oldham
2000 Feb 02
An iterator is any C++ thing that permits accessing items in a container using certain specified operations. Customary uses include walking through all of a container's items, printing each item or changing it to uppercase. An iterator is not a type and there is no special C++ syntax for an iterator. If it looks like an iterator, acts like an iterator, and quacks like an iterator, then it is an iterator.
Iterators were developed to permit writing code that works on many different types of containers. Many algorithms consist of just going through a container and looking at all its elements. For example, printing the elements of a container should not depend on the container; it only requires the ability to read all the elements in a container, printing each one. Other algorithms such as sorting are more complicated but again should not depend (much) on the container in which the items are stored. Most fast sorting algorithms require the ability to quickly access any element in a container. Thus, we need more powerful operations than just being able to visit all elements in a container.
Since iterators were designed to permit using any type of container, let us first make containers more concrete. A container is a collection of items. For example, a Tupperware container may hold a collection of peas or carrots or tofu. A sandwich bag container usually holds only sandwiches but can sometimes hold potato chips. A shish kebab stick is another type of container. Some containers such as Tupperware containers permit accessing any element in the container. Others do not; one can remove food from a shish kebob stick only at one end or the other. As the examples illustrate, a container is not a type nor is there special C++ syntax. If it looks like a container, acts like a container, and quacks like a container, then it is a container.
Standard Template Library containers we have already seen or will soon see include vectors, strings, pairs, and hash tables. One can also consider a stream as a container. By definition, a vector is an array that can change its size as needed. See the brief introduction, including the sample code, to vectors. A string is just a sequence of characters. The corresponding STL container is called string. Some examples of operations on strings are available.
One thing we frequently want to do to the elements in a container is print each element. It would be nice to be able to write code like
The two programs differ very little. In fact, I created the string code by replacing all vector occurrences with string and also changing the push_back function to +=. We do not need to know the type of v.begin() or v.end(). The compiler figures it out for us.
In the previous example, the same for_each function worked for vector and string containers. Since every C++ function corresponds to a single piece of code, we need to have common notation for walking through the contents of both vectors and strings. Let's sneak inside the implementation of for_each to see the notation and what is required of its iterator parameters.
for_each takes two input iterator parameters and one function parameter. The use of the template means the compiler figures out the type of the arguments so we do not have to write them explicitly. The first two arguments indicate the beginning and end of the container. We require that the end is exactly one past the last element in the container. If the container is empty, i.e., first == last, then we return the function argument. (The reason for returning the function argument is obscure; I have never used for_each's return value.) Otherwise, we apply the function argument each item in the container, starting with the first.
Whenever we have a templatized parameter, we should ask what operations the templatized parameter is required to support. The only use of the function parameter f is applying it to a container element. The input iterators are more interesting. We require three operations:
|++begin||move to the ``next'' item in the container|
|begin == end||compare two iterators to see if they point to the same place|
|*begin||read the item pointed to by the iterator|
We can specify anything we want as the first and second arguments to for_each as long as they can do these three operations. In other words, an input iterator ``lets us walk through the container looking at each element.'' We use the word ``input'' because, like regular keyboard input, we can only look at items sequentially, and we can only read, not change, them.
Table 1 has a complete list of operations input iterators are required to support. We will not discuss the last two operations for a while so do not worry about them; I included them only for completeness.
When printing items to output streams, we perform two operations: we want to write items to output and we want to move to the next position in the stream so we do not overwrite what we have just written. Output iterators for containers are similar. The operations we require are listed in Table 2. We are not permitted to read from an output iterator.
The copy function uses an output iterator as it copies elements from one container to another. Again, we go ``under the hood'' to see how copy works.
The operations on the first two iterators are ==, *first, and ++first so these two parameters can be input iterators. The operations on the last parameter are *result = value and ++result. These match the requirements we required for output iterators so this last parameter can be an output iterator. Note that we need only specify the beginning of the destination container; we just assume that it will be large enough to store the copies.
Here is code for copying from a vector of characters to a string of characters.
Since copy assumes that the destination string container is large enough, we have to resize it. See Section 11 for ways to automatically grow the string as the characters are inserted.
To summarize, an output iterator lets us walk through a container, writing each element.
Sometimes we want to walk through a container changing each item in the container. For example, we may want to fill a vector with zeroes. Here is an implementation for the fill function. In this function, the operations required by the iterator are ==, *first = value, and ++first.
Table 3 contains the complete list of operations required for forward iterators. Basically, this list is the union of operations on input and output iterators. We need not worry about -> and the last two operations.
Bidirectional iterators are forward iterators that also permit moving backwards one position at a time in the container. The reverse function requires bidirectional iterators. Bidirectional iterators support all the operations in Table 3 plus the two additional operations in Table 4.
Random access iterators are bidirectional iterators that can access any point in the container. In a constant amount of time, one can move to any position in a container using a random access iterator. This is useful when sorting or performing binary search.
Table 5 contains the additional operations that random access iterators provide. Random access iterators provide no additional power over bidirectional iterators except possibly faster performance.
There are two easy ways to create iterators:
The code vector<char>::const_iterator pos; explicitly declares an iterator for vectors of chars. The for loop applies the function to each item in the container.
In the spirit of C++, we complicate the syntax to enable the programmer to write faster code. A const_iterator is an ``constant'' iterator. That is, it permits walking through a container but gives only read access to container items. For example, one can ++iter and *iter, but not *iter = value.
Why use a const_iterator when an iterator will always work? First, we must use const_iterators to walk through constant containers. This frequently happens when a container is a const function parameter. Second, using consts can permit the compiler to write faster code. While code speed is not important for this class, it is a good habit to develop.
Since input and output streams are just collections of data, we can consider them as containers under the ``if it looks like a container, acts like a container, and quacks like a container, then it is a container'' rule. We only need iterators for streams.
To treat an istream in as an iterator, use istream_iterator<T>(in), where items of type T are to be read from the istream. The end-of-istream iterator is created using istream_iterator<T>().
To treat an ostream out as an iterator, use ostream_iterator<T>(out), where items of type T are to be read from the ostream. There is no end-of-ostream idea so there is not an analogous end-of-ostream iterator. To have a string delim printed after item is printed, use ostream_iterator<T>(out, delim).
Be sure to #include<iterator> to use these iterator adaptors.
Here is code to copy strings from the standard input to the standard output, printing them one per line. To treat the input as a container, we specify its beginning and ending using the first two arguments to copy. The last argument specifies each string is sent to cout separated by a newline character. For more information, read more about istream iterators and ostream iterators.
You are not responsible for knowing this material. In Section 4, we copied from a vector to a string, but we had to resize the string to ensure it was large enough to hold all the vector's characters. If the output iterator automatically enlarged the container every time a new item is added, we would not have to preallocate the container. In other words, we wish to insert elements using push_back. To do so, we can use an insert iterator like back_inserter(container).
Here is some code I claim is correct, but the current compiler does not have push_back for strings. This shows how very close to the cutting edge of C++ programming we are. Instead, we can copy from a string to a vector.
After reading all the characters into a string, these are copied into the vector v, which is enlarged by the back_inserter as characters are added.
For containers having a push_front operation, one can use front_inserter(container). To read more, see back inserter and front inserter WWW pages.
All other explanations of iterators known to me rely on readers already knowing about pointers even though it is not necessary to know about pointers to understand iterators. (In fact, I purposefully choose to introduce iterators before pointers.) The three best expositions I have found are: