torsdag 11. september 2014

[ C++11 - Part 4 ] STL - iterators

Standard Template Library

A major part of C++ is the Standard Template Library ( STL ). It is quite large and it contains both containers ( like vector, stack and maps ) with a lot of functionality for using these. In this post, we'll look at one part of the STL, iterator, and in the following post we'll look at other parts of STL like the algorithms library

Iterators

Iterators can be looked at like pointers. They reference an item in a data structure( including string ). They can also be used with streams. The iterators enables us to iterate through them using various operations like ++.

But we need different types of iterators because data structures can often be iterated in different ways. Some ways are efficient on some containers while they might be inefficient or even impossible on other containers.

The properties of the various iterators here are the bare minimum of the "pure" version of the iterators. Some implementations might support more operations.

Note that forward iterator, bidirectional iterator and random access iterator does not implement output iterators. This means that they can't, by definition, write to the object they refer to. We'll get back to this later.

Let's start by looking at the first type of iterator, output iterator.

Output iterator

As mentioned earlier, output iterator is a very limited type of iterator it is also the only iterator that can write to the object.

You can't go back with an output iterator, and you can't iterate over the same range twice. And there is no guarantee that you can write to the same position twice without incrementing the operator. And you can't compare two output iteratrs either. What you're basically doing is writing into a "black hole"; you're just writing and you can't see what you have written.

This might seem very constrictive, there's very little we can do. But these rules only apply in some cases, like when writing to streams, which we'll look at later. You can look at the iterators that follows everything described above as pureoutput iterators

Most iterators, though implement output iterators but also has other traits that enables more functionality. We'll look at these later.

A simple example

This is a very simple example of the intended usage for output iterators :

OutputIterator r2;
while ( ... )
{
*r2 = x;
r2++;
}

Or, in other words :

Write

Move forward

Write

Move forward

...

A bit more complex example

In C++ you have streams, which allows you to to either write or read from them using the >> and << operators. Examples of this is cout ( write ) cin ( read ). This makes coutoutput operators ( because they output to the terminal. )

cout is of the type ostream. You also have an iterator to this type, called ostream_iterator that can be set to an ostream Writing to this iterator writes to the ostream it's an iterator to.

Using this knowledge, we can create an iterator to cout, this enables us to write to the terminal using an iterator

#include < iterator >
#include < iostream >

int main()
{
// Create an ostream_iterator to std::cout.
// The "\n" means we add a line every time we write
std::ostream_iterator< int32_t > it(std::cout, "\n");

*it = 11;
it++;
*it = 44;
it++;
*it = 33;
it++;
}

Output :

11
44
33

The ostream_iterators are output iterators. Using cout like this shows how output iterators works ; you can only write and the writing has to be sequential. You can newer go back. Once you've written something, you can't "go back" and write something in the position before what you just wrote.

Input iterator

The counterpart of output iterators is input iterators. But where output iterators can only write, input iterators can read but there is also a small changes in the restrictions :

You can compare two iterators

Comparing two iterators is not guaranteed to work unless at least one of them is past the end ( like std::end )

Since you can read, you can also use the -> operator to access member variables

A simple example

The following example shows the essential use of an input iterator :

while( ... )
{
std::cout << *pos;
++pos;
}

Or, in other words :

Read

Move forward

Read

Move forward

...

A more complex example

In the previous example, we saw that ostream_iterators are output iteratorss. Which means we can use them to write to cout. Similar to this, we have istream_iterators that are input operators. And where we can use ostream_iterators with cout, we can use istream_iterators with cin which reads input.

Here too we have to work consecutively, we can't go back or skip ahead, we can read input in a similar to when we wrote to cout

Since we've specified int as the template argument of the input_iterator, the input_iterator ( intReader ) will become the same as invalid, which in C++ means the iterator will point to the element past the end element. This is the same as readerEnd and the loop will terminate.

Forward iterator

If you take a look at the iterator overview above, you'll see that the forward iterator implements output iterator. This means a forward iterator can do anything an output iterator can do but with a few more properties :

Two forward iterators are guaranteed to compare as equal if they refer to the same element

An input iterator can, as we saw above, be compared to another output iterator, but the operation is only guaranteed to be true if one of them is past the end of the container

The compare operation ( it1 == it2 ) is not guaranteed to be true for output iterators or input iterators, but it is for forward iterators

A forward iterator can also iterate forward as many times as you like before reading

With output iterators or input iterators we would have to read or write in between each time we iterate forward.

Example :

Forward_Iterator it1 = beginIterator;

// Iterate it1 forward a few steps
++it1;
++it1;
++it1;

// Use (*it1) for something
// ....

If we had used an output iterator or an input iterator to do the multiple iterations would have caused the iterators to become invalid.

Finally, a forward iterator can iterate over the same range twice.

output iterators or input iterators are intended to only iterate over the same range once ( single-pass ) but are intended to be able to do this ( multi-pass )

Example :

Forward_Iterator it1 = beginIterator;

// Iterate it1 forward a few steps
++it1;

// Use (*it1) for something
// ....

// Go back to beginIterator
it1 = beginIterator;

// Iterate it1 forward a few steps
++it1;

// Use (*it1) for something
// ...

Here we've effectively moved back and continued moving forward, which again would not be possible with code>output iterators or input iterators.
As you can see, using forward iterators is in many ways more convenient than using output iterators or input iterators, but some object only has very few operations, like the streams we looked at earlier.

Luckily, most ( if not all ) structures in the C++11 supports at least the functionality of forward iterators. Most of them provide iterators that has more properties, but this time we'll look at a structure that only provides forward iterators

Forward list

A linked list is a container that works a bit different from the array-like containers. Basically, each element has a pointer to the next element and previous. So that all the elements are chained together using pointers. If you want to go from one element to another, you use the next pointer ( or the previous if you want to go back ) This can mean a lot of operations if the list is very large.

The objects will not be one contiguous piece of memory like arrays are. This means there are no quick ways to go from the first to the last element. So if we wanted to go from Node 1 to Node 2 in the above illustration, we would have to go via Node 3. And if we wanted to go back from Node 3 to Node 1 we would have a problem. There is no pointer that goes back in a forward list. If you want to get to a previous node, you would need a pointer to an earlier element. The forard_list itself keeps a pointer to the first element in order to be able to access all elements.

A container in like this didn't exist in C++ until C++11 which has the container forward list.

Iterating forward lists

The forward list is a great way of showing how the forward iterators works. The forward list can only be iterated forward and the forward iterator can only iterate forwards.

Let's look at an example :

std::forward_list< int32_t > list( { 43, 63, 11, 0, 5 } );

// Get a forward iterator to the first element of list
std::forward_list<int32_t>::iterator it=std::begin(list);

We are now at the fifth element ( 5 ), but say we want to print the previous element ( 0 )? We need to go back to the begin element and increment like above.

// Go back to the first element
it = std::begin( list );

// Iterate forwards to the element before the one above
for ( int32_t i = 0 ; i < 3 ; ++i )
++it;

std::cout << (*it) << std::endl;

Output

43
63
11
0
5
0

Forward iterators and input iterator

Per definition, forward iterators does not include the properties of output iterators.

Most implementations of forward iterators does however support assignment like output iterators. These iterator are called mutableforward iterators

Bidirectional iterator

The bidirectional iterator is very similar to forward iterator. The only different is that it also supports backwards iteration. And that's all the difference between code>forward iterator and bidirectional iterator.

Linked list

In the forward iterator section we looked at singly linked lists which has a pointer to the next element. But most linked lists also has a pointer to the previous element.These are called doubly linked lists or simply just linked list.

The container type doubly linked lists are identical to singly linked lists, with the one exception that each Node now has a pointer to the previous Node too.

The prev pointer enables us to iterate backwards, so now we need an iterator that can iterate backwards and forwards. This is where the bidirectional iterator comes in.

An example

The following example is the similar to the example for forward list but this time we solve the last problem in a different way.

std::list< int32_t > list( { 43, 63, 11, 0, 5 } );

// Get a forward iterator to the first element of list
std::list< int32_t >::iterator it = std::begin( list );

We are now at the fifth element ( 5 ), but say we want to print the previous element (0 )? We can simply use the -- operator to iterate back;

--it;std::cout << (*it) << std::endl;

Output

43
63
11
0
5
0

As you can see, bidirectional iterators can iterate back and forth without problems. Now let's look at the final iterator, bidirectional iterator.

Random access iterator

The final iterator is also the one with the most features. It can be used like a pointer. In fact, some implementations of STL uses pointers as random access iterators.

Here's a list of the features of a random access iterator

All the features of bidirectional itearator

Random access

Access items by [] operator

it[n] returns the value of the position it + n

it[n] is the same as *( it + n )

Iterator arithmetics

This means you can add or subtract integer values and iterators

it += n moves it forward n steps

it -= n moves it backward n steps

it + n returns an iterator to the element n steps after it

n - it returns an iterator to the element n steps before it

it - it2 returns the distance between it and it2

Iterator comparison

This means you can check which if one iterator is before another iterator

it1 < it2 returns true if it1 is before it2

it1 > it2 returns true if it1 is after it2

it1 >= it2 returns true if it1 is not before it2

it1 >= it2 returns true if it1 is not after it2

not it1 < it2

Array-like containers

Array like containers are all containers that are organized as a continuous piece of memory. This means you can predict where the next item is, since it the next item will always be the previous item + the size of each item. Using this knowledge, it's easy to jump forward or backwards. An example of these containers is vector, which I covered in a previous part.

The above example shows how any array-like container ( vector, array, queue, deque, ... ) is laid out in memory. Everything is in one continuous block which means we always know where the next element is.

An example

The following example shows usage of random access iterators:

As you can see, the + operators of random access iterators are very useful. We will be using this for the upcoming post about STL algorithms.