An iterator allows you to traverse a container
int a[ SIZE ]; // fill a
for ( int i = 0; i < SIZE; ++i )
cout << a[ i ] << endl;
We will use procedural abstraction and data abstraction to iterate over a container
int main( )
{
List<int> il; // ( )
il.PushFront( 1 ); // ( 1 )
il.PushFront( 2 ); // ( 2 1 )
cout << il.Front( ); // 2
il.PopFront( ); // ( 1 )
il.PopFront( ); // ( )
cout << il.IsEmpty( ); // true
}
Our iterator will work a lot like using a pointer to traverse an array
int a[ SIZE ];
// Fill a with data, then print.
for ( int *p = a; p < a + SIZE; ++p )
cout << *p << endl;
List< int > b;
// Fill b with data, then print.
for ( List<int>::Iterator i = b.begin( ); i != b.end(); ++i )
cout << *i << endl;
List< int > b;
for ( /*1*/ List<int>::Iterator i = b.begin( ); /*4*/i != b.end( ); /*3*/++i )
cout << /*2*/*i << endl;
- Create an iterator with a constructor
- Get the
T
at the iterator's current position - Move the iterator to the next position
- Compare two iterators
We'll have two classes:
- One
List
class, that holdsT
's - One
Iterator
class, that returns eachT
in theList
There are two problems we need to solve:
- For the
Iterator
to do its job, it needs to have access to theNode
type, which is a private type insideList
- The
Iterator
needs to return a typeT
, which should be the exact same typeT
that is inside theList
We can solve both problems by declaring the Iterator
class inside the List
class
From the outside, we can refer to our new Iterator
class by specifying the scope: List<int>::Iterator
We can use a Node*
pointer to keep track of the current node
class Iterator
{
private:
Node *node_ptr;
public:
Iterator( ); // Create
T& operator*( ) const; // Get
Iterator& operator++( ); // Move
bool operator!=( Iterator rhs )
const; //Compare
};
The invariant on node_ptr
is that is points to the current node in the underlying List
, and nullptr
(same as NULL
or 0
) otherwise
List does not need to change
Iterator( ); // Create new Iterator
T &operator*( ) const; // Get T from node
Iterator &operator++( ); // Move to next node
bool operator!=( Iterator rhs ) const; // Compare two Iterators
int main( )
{
List<int> b; // fill b
for ( List<int>::Iterator i = b.begin( ); i != b.end( ); ++i )
cout << *i << endl;
}
Now we can define (implement) each Iterator
method
The constructor establishes the invariant on node_ptr
It's okay to implement short functions in the class declaration
class List
{
public:
class Iterator
{
Node *node_ptr;
public:
Iterator( ) : node_ptr( nullptr )
{
}
// "past the end" by
// default
// ...
};
};
The dereference operator returns a reference to the datum
at the current Iterator
position
class List
{
public:
class Iterator
{
Node *node_ptr;
public:
T &operator*( ) const
{
assert( node_ptr != nullptr );
return node_ptr->datum;
}
// ...
};
};
The assert()
stops execution is you try to use an iterator "pass the end"
The Iterator
has a pointer to the Node
created by the List
, and the Node
's data members are all public because it is a struct
The prefix increment operator moves to the next List
node
class List
{
public:
class Iterator
{
Node *node_ptr;
public:
Iterator &operator++( )
{
assert( node_ptr );
node_ptr = node_ptr->next;
return *this;
}
// ...
};
};
We need a way to compare two iterators
class List
{
// ...
class Iterator
{
Node *node_ptr;
public:
bool operator!=
( Iterator rhs ) const
{
return node_ptr !=
rhs.node_ptr;
}
// ...
};
};
We need to know where to start and where to end, and this ability is typically implemented as member functions called begin()
and end()
inside the container, not in the iterator
Returns a default Iterator
object, "past the end" position
class List
{
public:
class Iterator { /*...*/ };
Iterator end( ) const
{
return Iterator( );
}
};
Returns an Iterator
object pointing to the first List
position
class List
{
Node *first; // points to
// first Node
// ...
class Iterator { /*...*/ };
Iterator begin( ) const
{
return Iterator( first );
}
};
We need another Iterator
constructor with a Node*
input
No one outside List
should see this, so make it private
class List
{
public:
class Iterator
{
// ...
private:
Iterator( Node* p ) : node_ptr( p )
{
}
};
Iterator begin( ) const
{
return Iterator( first );
}
};
Iterator( Node* p )
is private to theIterator
classbegin( )
is a member function ofList
, notIterator
- Therefore,
begin( )
cannot accessIterator( Node* p )
The friend
declaration allows you to expose the private members of one class to another class (and only that class)
class List
{
public:
class Iterator
{
// ...
private:
Iterator( Node* p ) :
node_ptr( p ) { }
friend class List;
};
Iterator begin( ) const
{
return Iterator( first );
}
};
Now, begin()
can access Iterator(Node* p)
, but clients of List
cannot
Understanding that friendship is something given, not taken, will help you remember that "friend class List;
" goes inside Iterator
, not the other way around
List<int> il;
il.PushFront( 3 );
il.PushFront( 2 );
il.PushFront( 1 );
for ( List<int>::Iterator i = il.begin( ); i != il.end( ); ++i )
cout << *i << " ";
cout << endl;
./a.out
1 2 3
Range for automatically starts at il.begin()
and ends before il.end()
Range for works with any container that has a "standard" iterator like our List
List<int> il;
// fill il
for ( auto i:il )
// i is as copy of a datum
cout << i << " ";
cout << endl;
./a.out
1 2 3
This allows us to write any number of algorithms that must examine every entry of a list, without:
- Needing to udnerstand the mechanics of how
List
s actually work - Requiring that the designer of the
List
class knew everything that the client wanted in advance
In other words, we have successfully created a function abstraction to access a container
Sine the Iterator
is an object, we can have more than one pointing to the same List
List<int> il; //fill with ( 2 3 )
auto i = il.begin( ); // List<int>::Iterator
auto j = i; // List<int>::Iterator
++j;
cout << "*i = " << *i << endl;
cout << "*j = " << *j << endl;
./a.out
*i = 2
*j = 3
A function that checks a list for duplicates
Below is code that works for an array
// EFFECTS: returns true if a
// contains no duplicates
bool HasNoDuplicates( int a[ ], int size )
{
for ( int i = 0; i < size; ++i )
for ( int j = i + 1; j < size; ++j )
if ( a[ i ] == a[ j ] )
return false;
return true;
}
Below is code that works for List
bool HasNoDuplicates( const List<int> &l )
{
for ( auto i=il.begin( ); i != il.end( ); ++i )
{
auto j = i;
++j; //this will increment an iterator
for ( ; j != il.end( ); ++j )
if ( *i == *j )
return false;
}
return true;
}
We cannot use Range for with i
, because i
would be an int
, not an Iterator
Do we need it? Iterator
has one member variable Node *node_ptr
, which is a pointer to dynamically allocated memory
No, because memory is already managed by the List
:
- If you allocated the memory with
new
, you need to deallocate it withdelete
Iterator
does not allocate memory, and therefore does not need to deallocateList
allocates memory for its nodes, and also deallocates
Once an iterator is created, if the underlying container is modified, the iterator may become invalid, dependent on the container
If the iterator is invalidated, its behavior is undefined, much like an invalid pointer
The intuition behind this is that the iterator depends on the representation of the container - if that changes, the iterator is likely to miss an element or return an element that no longer exists
It is your responsibility to keep your Iterator
within the bounds of your container
#include "List.h"
int main( )
{
List<int> il; //fill with ( 2 3 )
auto i = il.begin( );
il.PopFront( );
cout << *i; //Undefined!
}
- Iterators have a lot in common with pointers
- Both can refer to objects that don't exist any more
- Be careful!