C++ Iterators

Understanding pointers to container elements for traversal

🔄 What are Iterators?

Iterators are objects that act like pointers to elements in containers. They provide a uniform way to traverse and access elements in different container types like vectors, sets, and maps.


#include <vector>
#include <iostream>
using namespace std;

int main() {
    vector<int> nums = {10, 20, 30};
    
    // Using iterator to traverse
    for (auto it = nums.begin(); it != nums.end(); ++it) {
        cout << *it << " "; // Output: 10 20 30
    }
    return 0;
}
                                    

Types of Iterators

➡️

Forward Iterator

Move forward through container

++it; // Move to next element
↔️

Bidirectional Iterator

Move forward and backward

++it; --it; // Both directions
🎯

Random Access Iterator

Jump to any position directly

it + 5; it[3]; // Jump anywhere
🔄

Reverse Iterator

Traverse container backwards

rbegin(), rend() // Reverse

🔹 Basic Iterator Usage

Iterators act as pointers to container elements, enabling traversal and access without indexing. They come in types like forward, bidirectional, and random-access, each supporting different operations. Common usage includes looping with begin() and end(), dereferencing to get values, and incrementing to move. Iterators are foundational for standard algorithms, allowing functions like find() or sort() to work uniformly across various containers.

#include <vector>
#include <iostream>
using namespace std;

int main() {
    vector<string> fruits = {"apple", "banana", "cherry"};
    
    // Method 1: Traditional iterator loop
    cout << "Method 1 - Traditional iterator:" << endl;
    for (vector<string>::iterator it = fruits.begin(); it != fruits.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;
    
    // Method 2: Auto keyword (recommended)
    cout << "Method 2 - Auto iterator:" << endl;
    for (auto it = fruits.begin(); it != fruits.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;
    
    // Method 3: Range-based for loop (easiest)
    cout << "Method 3 - Range-based:" << endl;
    for (const string& fruit : fruits) {
        cout << fruit << " ";
    }
    
    return 0;
}

Output:

Method 1 - Traditional iterator:
apple banana cherry
Method 2 - Auto iterator:
apple banana cherry
Method 3 - Range-based:
apple banana cherry

🔹 Iterator Operations

Key iterator operations include dereferencing, incrementing, decrementing, and comparison. Random-access iterators (for vectors and deques) also support arithmetic like addition and subtraction. Bidirectional iterators (for lists) allow forward and backward movement. Operations ensure safe navigation: always check against end() before dereferencing. Using these operations correctly prevents undefined behavior and is crucial for implementing robust, efficient loops and algorithms.

#include <vector>
#include <iostream>
using namespace std;

int main() {
    vector<int> numbers = {10, 20, 30, 40, 50};
    
    // Get iterators
    auto begin_it = numbers.begin();    // Points to first element
    auto end_it = numbers.end();        // Points past last element
    
    cout << "First element: " << *begin_it << endl;        // 10
    
    // Move iterator
    ++begin_it;
    cout << "Second element: " << *begin_it << endl;       // 20
    
    // Iterator arithmetic (for random access iterators)
    auto third_it = numbers.begin() + 2;
    cout << "Third element: " << *third_it << endl;        // 30
    
    // Distance between iterators
    auto distance = end_it - numbers.begin();
    cout << "Container size: " << distance << endl;        // 5
    
    // Modify through iterator
    *third_it = 99;
    cout << "Modified third element: " << numbers[2] << endl; // 99
    
    return 0;
}

Output:

First element: 10
Second element: 20
Third element: 30
Container size: 5
Modified third element: 99

🔹 Reverse Iterators

Reverse iterators traverse a container from the last element to the first, simplifying reverse-order processing. Obtained via rbegin() and rend(), they internally adapt normal iterators. Incrementing a reverse iterator moves backward through the sequence. They are useful for algorithms that need to process data in opposite order, like searching from the end or displaying a list backwards, without manually managing indices or decrement operations.

#include <vector>
#include <iostream>
using namespace std;

int main() {
    vector<char> letters = {'A', 'B', 'C', 'D', 'E'};
    
    cout << "Forward traversal: ";
    for (auto it = letters.begin(); it != letters.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;
    
    cout << "Reverse traversal: ";
    for (auto rit = letters.rbegin(); rit != letters.rend(); ++rit) {
        cout << *rit << " ";
    }
    cout << endl;
    
    // Access last element using reverse iterator
    cout << "Last element: " << *letters.rbegin() << endl;    // E
    cout << "First element: " << *letters.begin() << endl;    // A
    
    return 0;
}

Output:

Forward traversal: A B C D E
Reverse traversal: E D C B A
Last element: E
First element: A

🔹 Iterators with Different Containers

Iterators behave differently based on the container type, affecting their capabilities and performance. Associative containers (e.g., std::set, std::map) provide bidirectional iterators that traverse in sorted order. Sequence containers offer random-access (vector, deque) or bidirectional (list) iterators. Unordered containers provide forward iterators. Understanding these distinctions ensures you choose the right container and iterator for efficient data access and manipulation in your C++ programs.

#include <set>
#include <map>
#include <iostream>
using namespace std;

int main() {
    // Iterator with set
    set<int> numbers = {30, 10, 50, 20};
    cout << "Set elements (sorted): ";
    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;
    
    // Iterator with map
    map<string, int> ages = {{"Alice", 25}, {"Bob", 30}, {"Charlie", 22}};
    cout << "Map key-value pairs:" << endl;
    for (auto it = ages.begin(); it != ages.end(); ++it) {
        cout << it->first << ": " << it->second << endl;
    }
    
    // Find specific element
    auto found = ages.find("Bob");
    if (found != ages.end()) {
        cout << "Found Bob, age: " << found->second << endl;
    }
    
    return 0;
}

Output:

Set elements (sorted): 10 20 30 50
Map key-value pairs:
Alice: 25
Bob: 30
Charlie: 22
Found Bob, age: 30

🔹 Iterator Best Practices

To use iterators effectively, always validate them before dereferencing and beware of invalidation. Operations like insert() or erase() can invalidate iterators for some containers (especially vectors and deques). Use the return values of these functions to obtain valid iterators. Prefer range-based for loops for simple traversal, and use algorithm functions like std::for_each to avoid manual iterator management where possible, reducing errors.

Iterator Guidelines:

  • Use auto: Let compiler deduce iterator type
  • Prefer range-based loops: Simpler and safer
  • Check validity: Always compare with end() before dereferencing
  • Don't modify during iteration: Can invalidate iterators
  • Use const_iterator: When you don't need to modify elements

Common Iterator Functions:

  • begin(), end(): Forward iteration range
  • rbegin(), rend(): Reverse iteration range
  • cbegin(), cend(): Const forward iterators
  • advance(it, n): Move iterator n positions
  • distance(it1, it2): Count elements between iterators

🧠 Test Your Knowledge

What does the * operator do with an iterator?