When we have a collection of objects, one of the most common things we need to do in our code is access all the objects in that collection.
Our earlier lessons on arrays, vectors and linked lists showed ways we can do that. Unfortunately, the methods needed to be tailored to the type of container we were working with. With arrays, we could use the []
operator to access elements directly.
With linked lists, we didn’t have that ability, because they work differently. Yet more container types in the future will work differently again.
It would be nice if we could have a standardized way to iterate over our collections, regardless of what type of collection it is. This is where iterators come in.
Iterators are objects that are designed to move through a container, providing access to all the items within that container as it passes through. Iterators can have many capabilities, but the most fundamental are:
*
operator to grant access to an element in the collection++
operator, to move on to the next elementPointers implement the *
operator for access to the thing they are pointing at.
Additionally, when we’re using a data structure that keeps its items in contiguous memory, the ++
operator will update the pointer to the memory location of the next item in the structure.
As a result, simple pointers can be used as array iterators. We’ll call our iterator It
for brevity:
#include <array>
#include <iostream>
using namespace std;
int main()
{
array MyArray { 1, 2, 3 };
int* It { &MyArray[0] };
cout << *It;
++It;
cout << *It;
++It;
cout << *It;
}
123
We can enhance this to use a loop instead, which will allow it to work for any length of the array. We’ll start our iterator at the beginning of the array, and continue incrementing it until we reach the end:
#include <array>
#include <iostream>
using namespace std;
int main()
{
array MyArray { 1, 2, 3 };
int* Begin { &MyArray[0] };
int* End { Begin + std::size(MyArray) };
for (auto It { Begin }; It != End; ++It) {
cout << *It;
}
}
123
To simplify things, the standard library array
class comes with begin
and end
methods, so we can use those rather than creating our own.
Let's update our previous code to use the standard library’s array methods instead:
#include <array>
#include <iostream>
using namespace std;
int main()
{
array Nums { 1, 2, 3 };
for (auto It { Nums.begin() }; It != Nums.end(); ++It) {
cout << *It;
}
}
123
Now, we can begin to benefit from standardisation. Even though a linked list doesn’t store its objects in the same way as an array, our code will still work if we switch out our array for a linked list.
This is because, just like std::array
, std::forward_list
has a begin
and end
method, and those methods return an object that behaves exactly the same way as the one returned from the std::array
version of those functions.
So, we can change our data structure, without changing our iteration code:
#include <forward_list>
#include <iostream>
using namespace std;
int main()
{
forward_list Nums { 1, 2, 3 };
for (auto It { Nums.begin() }; It != Nums.end(); ++It) {
cout << *It;
}
}
123
Almost all standard library containers we’ll see work this way, and we can set up our own custom types to work this way too.
This standardisation has many benefits, and we’ll see those in more detail in the algorithms chapter of this course. But, there is an immediate benefit we can start using now.
With containers that support begin
and end
, there is a more concise way to iterate over them. A range-based for loop is the preferred way to iterate over containers:
#include <array>
#include <iostream>
using namespace std;
int main()
{
array Nums { 1, 2, 3 };
for (auto Num : Nums) {
cout << Num;
}
}
123
By convention, we use auto
when using a range-based for loop. Range-based for loops will copy the item into the body of the loop. As a result, this will still log out 123
, as our first loop is modifying copies:
#include <array>
#include <iostream>
using namespace std;
int main()
{
array Nums { 1, 2, 3 };
for (auto Num : Nums) {
++Num;
}
for (auto Num : Nums) {
cout << Num;
}
}
123
Generally, we want to pass by reference instead, so we’re not making unnecessary copies of our objects. We can do that using the &
token in the usual way:
#include <array>
#include <iostream>
using namespace std;
int main()
{
array Nums { 1, 2, 3 };
for (auto& Num : Nums) {
++Num;
}
for (auto Num : Nums) {
cout << Num;
}
}
234
When we don’t need to modify the items in the collection, we should mark our reference as const
:
#include <array>
#include <iostream>
using namespace std;
int main()
{
array Nums { 1, 2, 3 };
for (const auto& Num : Nums) {
cout << Num;
}
}
123
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.