C++ - C++ Iterators: Types, Operations, and Practical Usage

Introduction

An iterator in C++ is an object that provides a way to access and move through elements of a data structure, particularly containers provided by the Standard Template Library (STL). Iterators are similar to pointers because they can point to an element and can often be dereferenced using the * operator. However, iterators are more general than ordinary pointers because they provide a common interface for working with different types of containers.

For example, a program may need to process every element in a container without knowing exactly how that container stores its data internally. An iterator allows the program to move from one element to another using operations such as incrementing, decrementing, dereferencing, and comparison. The C++ standard library defines different iterator categories according to the operations they support.

Why Are Iterators Important?

Different C++ containers use different internal storage mechanisms. An array stores elements in contiguous memory, while a linked list stores elements in separate nodes connected through links. A programmer should not need to understand these internal implementation details every time elements are processed.

Iterators provide a standardized mechanism for traversing these containers. This allows many STL algorithms to work with different containers using the same programming approach.

For example:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> numbers = {10, 20, 30, 40, 50};

    std::vector<int>::iterator it;

    for (it = numbers.begin(); it != numbers.end(); ++it) {
        std::cout << *it << " ";
    }

    return 0;
}

Here:

numbers.begin()

returns an iterator referring to the first element.

numbers.end()

returns an iterator representing the position just beyond the last element.

The expression:

*it

accesses the value at the iterator's current position.

The expression:

++it

moves the iterator to the next position.

The end() iterator is a past-the-end value and should not be dereferenced. 

Basic Iterator Operations

Several operations are commonly associated with iterators.

1. Dereferencing

The dereference operator * is used to access the element to which an iterator refers.

std::vector<int> numbers = {10, 20, 30};

auto it = numbers.begin();

std::cout << *it;

Output:

10

If the iterator is incremented:

++it;
std::cout << *it;

the output becomes:

20

An iterator must be dereferenceable before *it is used. A past-the-end iterator is not dereferenceable. 

2. Incrementing

The ++ operator moves an iterator forward.

++it;

The postfix form is also possible:

it++;

For many iterator categories, incrementing is the fundamental operation used to move through a sequence.

3. Decrementing

Some iterators support the -- operator.

--it;

This moves the iterator toward the previous element.

However, not every iterator supports decrementing. Bidirectional iterators and stronger iterator categories support backward movement. 

4. Comparison

Iterators can often be compared to determine whether traversal has reached a particular position.

A common pattern is:

for (auto it = numbers.begin(); it != numbers.end(); ++it) {
    std::cout << *it << " ";
}

The loop continues until it becomes equal to numbers.end().

Main Iterator Categories

C++ defines several iterator categories based on the operations they support. The traditional categories are Input, Output, Forward, Bidirectional, and Random Access. Since C++17, Contiguous Iterator is also recognized as a distinct category. 

The categories can be understood as a progression of capabilities:

Input Iterator
      |
Forward Iterator
      |
Bidirectional Iterator
      |
Random Access Iterator
      |
Contiguous Iterator

Output iterators are separate because their primary purpose is writing values rather than reading them. 

1. Input Iterator

An Input Iterator is primarily used for reading values sequentially.

It supports operations such as:

*it
++it
it++

A simple conceptual example is:

auto it = container.begin();

while (it != container.end()) {
    std::cout << *it << "\n";
    ++it;
}

Input iterators are designed for sequential traversal and do not provide the full set of operations available from stronger iterator categories.

They are particularly useful when an algorithm only needs to read elements while moving forward through a sequence. 

2. Output Iterator

An Output Iterator is primarily used to write values into a destination.

For example:

*it = 100;

writes a value through the iterator.

Output iterators support writing and advancing, but they are not designed for general-purpose reading. 

A simple conceptual example is:

std::vector<int> numbers(3);

auto it = numbers.begin();

*it = 10;
++it;

*it = 20;
++it;

*it = 30;

After this operation, the vector contains:

10 20 30

3. Forward Iterator

A Forward Iterator provides everything available from an input iterator while additionally supporting multi-pass traversal.

This means that multiple iterators can traverse the same sequence independently rather than being limited to a single-pass use case. C++20 formally expresses this through the std::forward_iterator concept.

A forward iterator supports:

*it
++it

and comparison operations required for forward traversal.

Forward iterators are useful when an algorithm needs to make multiple passes through a sequence.

4. Bidirectional Iterator

A Bidirectional Iterator supports movement in both directions.

In addition to forward movement:

++it;

it supports backward movement:

--it;

For example:

auto it = container.end();

--it;
std::cout << *it;

This can be useful when processing elements from both directions.

A bidirectional iterator builds on the capabilities of a forward iterator. 

5. Random Access Iterator

A Random Access Iterator provides significantly more powerful navigation.

It can move forward and backward and can jump directly by a specified number of positions.

For example:

it + 5

moves five positions forward.

You can also use:

it - 2

or:

it[3]

and compare iterator positions.

For example:

std::vector<int> numbers = {10, 20, 30, 40, 50};

auto it = numbers.begin();

std::cout << *(it + 3);

Output:

40

Random-access iterator operations such as advancing by an offset and subscripting are designed to work in constant time for appropriate iterators. 

6. Contiguous Iterator

A Contiguous Iterator is the strongest standard iterator category in the traditional hierarchy. It represents iterators whose referenced elements are stored contiguously in memory.

This category was formally introduced in C++17. Iterators for containers such as std::vector, std::array, and std::basic_string can satisfy contiguous-iterator requirements when applicable. 

For example:

std::vector<int> numbers = {10, 20, 30, 40};

auto it = numbers.begin();

std::cout << *it;

The elements of a vector are stored contiguously, which enables random-access operations and efficient interaction with memory-oriented operations.

Iterator Categories and Capabilities

The major differences can be summarized as follows:

Iterator Read Write Forward Backward Random Access
Input Yes Not generally Yes No No
Output No Yes Yes No No
Forward Yes Depending on iterator Yes No No
Bidirectional Yes Depending on iterator Yes Yes No
Random Access Yes Depending on iterator Yes Yes Yes
Contiguous Yes Depending on iterator Yes Yes Yes

The important point is that iterator categories are defined by the operations an iterator supports rather than simply by its specific C++ type. A pointer, for example, can satisfy random-access and contiguous iterator requirements when used appropriately. 

Iterators and STL Containers

Different containers provide different iterator capabilities.

For example, a std::vector provides random-access iterators, allowing operations such as:

it + 5

and:

it - 2

A linked-list-style container such as std::list provides bidirectional traversal, but it does not provide random-access operations such as:

it + 5

This difference is important when selecting algorithms and understanding their performance.

Using auto with Iterators

Older C++ programs often declare iterator types explicitly:

std::vector<int>::iterator it;

Modern C++ frequently uses auto:

auto it = numbers.begin();

This makes code shorter and avoids manually specifying a potentially complicated iterator type.

A complete example is:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> numbers = {10, 20, 30, 40, 50};

    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        std::cout << *it << " ";
    }

    return 0;
}

Output:

10 20 30 40 50

Constant Iterators

Sometimes a program should be able to read elements but should not modify them through the iterator.

For this purpose, C++ provides constant iterators such as:

std::vector<int>::const_iterator

Example:

std::vector<int> numbers = {10, 20, 30};

for (auto it = numbers.cbegin(); it != numbers.cend(); ++it) {
    std::cout << *it << " ";
}

Here, cbegin() and cend() provide constant iterators.

An important advantage is that code using the iterator cannot modify the elements through that iterator.

Iterator Invalidation

One of the most important practical concepts is iterator invalidation.

An iterator may become invalid after a container is modified. For example, operations that cause a std::vector to reallocate its storage can invalidate existing iterators.

Consider:

std::vector<int> numbers = {10, 20, 30};

auto it = numbers.begin();

numbers.push_back(40);

Depending on whether reallocation occurs, the previously stored iterator may no longer be valid.

Therefore, programmers must understand the invalidation rules of the particular container being used. This is especially important when iterators are stored and the container is modified during processing.

Iterators with Standard Algorithms

One of the biggest advantages of iterators is that they allow STL algorithms to operate on ranges.

For example:

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> numbers = {50, 20, 40, 10, 30};

    std::sort(numbers.begin(), numbers.end());

    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        std::cout << *it << " ";
    }

    return 0;
}

Output:

10 20 30 40 50

The algorithm receives two iterators:

numbers.begin()
numbers.end()

These define the range on which the algorithm operates.

This iterator-based design is one of the foundations of the STL because algorithms can operate on ranges without needing to know the complete internal implementation of the container.

Iterator Operations from <iterator>

C++ provides several utilities in the <iterator> header. These include iterator traits, iterator category tags, iterator adaptors, and operations for working with iterators. 

For example:

std::advance(it, 3);

moves an iterator forward by three positions where the iterator supports the required operation.

Another useful operation is:

std::distance(first, last);

which determines the distance between two iterators.

Example:

std::vector<int> numbers = {10, 20, 30, 40, 50};

auto first = numbers.begin();
auto last = numbers.end();

std::cout << std::distance(first, last);

Output:

5

The implementation and efficiency of such operations depend on the iterator category. For example, random-access iterators can calculate distances directly, while weaker iterators may need to advance through the sequence. 

Iterators in Modern C++

Modern C++ has introduced formal iterator concepts such as:

std::input_iterator
std::output_iterator
std::forward_iterator
std::bidirectional_iterator
std::random_access_iterator
std::contiguous_iterator

These concepts provide compile-time ways to express the capabilities required by generic code. C++20 also provides concepts such as input_or_output_iterator and sentinel_for

This makes it easier to write generic code that clearly states what kind of iterator it requires.

Practical Example

Consider a program that needs to find and modify values greater than 50:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> numbers = {20, 60, 40, 80, 30, 90};

    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        if (*it > 50) {
            *it = 0;
        }
    }

    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        std::cout << *it << " ";
    }

    return 0;
}

Output:

20 0 40 0 30 0

The iterator provides both access to the current element and movement through the vector. The expression:

*it = 0;

modifies the element being referenced.

Common Mistakes

Dereferencing end()

This is incorrect:

auto it = numbers.end();
std::cout << *it;

end() represents the position after the last element and should not be dereferenced. 

Using an Invalidated Iterator

Modifying a container can invalidate existing iterators depending on the container and operation. Always check the relevant container's iterator invalidation rules.

Using Unsupported Operations

Not every iterator supports random access.

For example, code such as:

it + 5

should only be used when the iterator category supports that operation.

Confusing Iterators with Indexes

An iterator is not necessarily an integer index.

This:

numbers[i]

uses an index, whereas:

*it

uses an iterator.

Iterator-based programming is more general because it can work with containers that do not provide indexing.

Advantages of Iterators

The major advantages of iterators are:

  1. They provide a common way to traverse different containers.

  2. They allow STL algorithms to operate on ranges.

  3. They hide many container implementation details.

  4. They support generic programming.

  5. Different iterator categories allow algorithms to use the strongest operations available.

  6. They can improve code reusability by separating algorithms from container implementations.

  7. They allow both sequential and, where supported, random-access traversal.

Conclusion

Iterators are a fundamental part of C++ and the STL. They provide a standardized interface for accessing and traversing elements without requiring algorithms to know how a container stores those elements. Their capabilities range from simple sequential reading and writing to bidirectional movement, random access, and contiguous-memory access. 

Understanding iterator categories is particularly important because it helps programmers choose appropriate operations and understand algorithm performance. Input and output iterators provide basic traversal capabilities, forward iterators support multi-pass traversal, bidirectional iterators add backward movement, random-access iterators provide direct positional movement, and contiguous iterators additionally guarantee contiguous storage. 

For students learning C++, the most important practical pattern to remember is:

for (auto it = container.begin(); it != container.end(); ++it) {
    std::cout << *it;
}

This simple pattern demonstrates the three fundamental iterator operations: obtaining an iterator, dereferencing it to access an element, and incrementing it to move through the container.