Module 6.2

STL Iterators

Iterators are the glue that connects containers to algorithms in the STL. They provide a uniform way to traverse and manipulate elements in any container, making your code more generic and reusable!

40 min read
Intermediate
Hands-on Examples
What You'll Learn
  • Iterator basics - begin(), end(), and traversal
  • Iterator categories and their capabilities
  • Iterator operations - advance, distance, next, prev
  • Reverse and const iterators
  • Iterator best practices and common patterns
Contents
01

Introduction to Iterators

Iterators are generalized pointers that provide a uniform way to access elements in any STL container. They abstract away the differences between container implementations, allowing you to write code that works with vectors, lists, sets, and maps using the same syntax.

What is an Iterator?

Think of an iterator as a smart bookmark that knows how to move through a container. Just like a finger pointing at items in a list, an iterator points to a specific element and knows how to move to the next one. The key difference from regular pointers is that iterators work uniformly across all container types.

STL Concept

Iterator

An iterator is an object that points to an element in a container and provides operations to move through the container's elements. It generalizes the concept of pointers to work with any container type in a consistent way.

Iterators are the bridge between containers and algorithms - algorithms work on iterator ranges, not on containers directly. This design allows the same algorithm to work with vectors, lists, sets, and even custom containers.

Key operations: Dereference (*it), Increment (++it), Compare (it1 != it2), and for some iterators: Decrement (--it) and Arithmetic (it + n).

Basic Iterator Usage

Every STL container provides begin() and end() member functions that return iterators. The begin() iterator points to the first element, while end() points to one position past the last element, creating a half-open range [begin, end).

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

int main() {
    vector<int> numbers = {10, 20, 30, 40, 50};
    
    // Get iterators to beginning and end
    vector<int>::iterator it = numbers.begin();
    vector<int>::iterator end = numbers.end();
    
    // Traverse using iterators
    cout << "Elements: ";
    while (it != end) {
        cout << *it << " ";  // Dereference to get value
        ++it;                // Move to next element
    }
    cout << endl;
    // Output: Elements: 10 20 30 40 50
    
    return 0;
}

This example shows the fundamental iterator pattern: get the begin and end iterators, then loop while they're not equal, dereferencing to access values and incrementing to move forward. The *it syntax dereferences the iterator (like a pointer), and ++it moves to the next element.

Modern C++ with auto

In modern C++ (C++11 and later), you can use the auto keyword to avoid writing out the full iterator type. This makes code cleaner and less error-prone. You can also use range-based for loops which handle iterators automatically behind the scenes.

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

int main() {
    vector<string> fruits = {"apple", "banana", "cherry"};
    
    // Using auto - much cleaner!
    cout << "With auto: ";
    for (auto it = fruits.begin(); it != fruits.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;  // apple banana cherry
    
    // Range-based for loop - even cleaner!
    cout << "Range-based: ";
    for (const auto& fruit : fruits) {
        cout << fruit << " ";
    }
    cout << endl;  // apple banana cherry
    
    // Range-based is equivalent to this iterator code:
    // for (auto it = fruits.begin(); it != fruits.end(); ++it) {
    //     const auto& fruit = *it;
    //     cout << fruit << " ";
    // }
    
    return 0;
}

The auto keyword lets the compiler deduce the iterator type automatically. Range-based for loops are syntactic sugar that use iterators internally - they call begin() and end() and handle the dereferencing for you. Use range-based loops when you need to visit every element, and explicit iterators when you need more control.

The Half-Open Range [begin, end)

A crucial concept in C++ iterators is the half-open range notation [begin, end). This means the range includes the element at begin but excludes the element at end. The end() iterator points to one position past the last valid element.

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

int main() {
    vector<int> nums = {1, 2, 3, 4, 5};
    
    // Visual representation:
    // Elements:  [1]  [2]  [3]  [4]  [5]  [ ]
    // Index:      0    1    2    3    4   
    // Iterator: begin                    end
    
    cout << "begin points to: " << *nums.begin() << endl;  // 1
    // cout << *nums.end();  // UNDEFINED BEHAVIOR! Don't do this!
    
    // Check if container is empty using iterators
    if (nums.begin() == nums.end()) {
        cout << "Container is empty" << endl;
    } else {
        cout << "Container has " << nums.size() << " elements" << endl;
    }
    
    // Empty container: begin() == end()
    vector<int> empty;
    cout << "Empty check: " << (empty.begin() == empty.end()) << endl;  // 1 (true)
    
    return 0;
}

The half-open range design has elegant properties: an empty container has begin() == end(), the number of elements equals end - begin for random access iterators, and you never accidentally dereference past the end if your loop condition is it != end. Never dereference end() - it doesn't point to a valid element!

Why Use Iterators?
  • Work with any container type uniformly
  • Required by STL algorithms
  • More expressive than index-based loops
  • Support const-correctness patterns
Common Mistakes
  • Dereferencing end() iterator
  • Using invalidated iterators
  • Comparing iterators from different containers
  • Off-by-one errors with ranges
Modifying Elements Through Iterators

Iterators don't just read values - you can also modify container elements through them. This is essential for in-place transformations and algorithms that need to change data.

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

int main() {
    vector<int> numbers = {1, 2, 3, 4, 5};
    
    // Double each element using iterator
    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        *it = *it * 2;  // Modify through dereference
    }
    
    cout << "Doubled: ";
    for (int n : numbers) cout << n << " ";
    cout << endl;  // Doubled: 2 4 6 8 10
    
    // Using reference in range-based for
    for (auto& num : numbers) {
        num += 10;  // Modify directly through reference
    }
    
    cout << "Added 10: ";
    for (int n : numbers) cout << n << " ";
    cout << endl;  // Added 10: 12 14 16 18 20
    
    return 0;
}

When you dereference an iterator with *it, you get a reference to the element, not a copy. This means *it = value modifies the actual container element. In range-based for loops, use auto& (reference) to modify elements or const auto& to just read them efficiently.

Practice Questions: Introduction to Iterators

Task: Create a vector of 5 integers. Use explicit iterators (not range-based for) with begin() and end() to print all elements. Then modify each element by multiplying it by 2 and print again.

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

int main() {
    vector<int> nums = {10, 20, 30, 40, 50};
    
    // Print using iterators
    cout << "Original: ";
    for (vector<int>::iterator it = nums.begin(); it != nums.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;
    // Output: Original: 10 20 30 40 50
    
    // Modify each element
    for (auto it = nums.begin(); it != nums.end(); ++it) {
        *it = *it * 2;
    }
    
    // Print again
    cout << "Doubled: ";
    for (auto it = nums.begin(); it != nums.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;
    // Output: Doubled: 20 40 60 80 100
    
    return 0;
}

Task: Create a vector of strings containing 4 fruit names. Use auto to declare iterators and print each fruit with its index position (0, 1, 2, 3).

Show Solution
#include <iostream>
#include <vector>
#include <string>
using namespace std;

int main() {
    vector<string> fruits = {"apple", "banana", "cherry", "date"};
    
    int index = 0;
    for (auto it = fruits.begin(); it != fruits.end(); ++it) {
        cout << "Index " << index << ": " << *it << endl;
        index++;
    }
    // Output:
    // Index 0: apple
    // Index 1: banana
    // Index 2: cherry
    // Index 3: date
    
    // Alternative: calculate index from iterator distance
    cout << "\nUsing distance:" << endl;
    for (auto it = fruits.begin(); it != fruits.end(); ++it) {
        cout << "Index " << (it - fruits.begin()) << ": " << *it << endl;
    }
    
    return 0;
}

Task: Create a vector with values {5, 10, 15, 20, 25, 30}. Write code using iterators to find the first element greater than 17 and print its value and position. Handle the case where no such element exists.

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

int main() {
    vector<int> nums = {5, 10, 15, 20, 25, 30};
    int target = 17;
    
    // Search using iterators
    auto it = nums.begin();
    while (it != nums.end() && *it <= target) {
        ++it;
    }
    
    // Check if found
    if (it != nums.end()) {
        int position = it - nums.begin();  // Works for random access iterators
        cout << "First element > " << target << ": " << *it << endl;
        cout << "Position (index): " << position << endl;
    } else {
        cout << "No element greater than " << target << " found" << endl;
    }
    // Output:
    // First element > 17: 20
    // Position (index): 3
    
    // Test with target = 50 (no element found)
    target = 50;
    it = nums.begin();
    while (it != nums.end() && *it <= target) {
        ++it;
    }
    if (it == nums.end()) {
        cout << "\nNo element greater than " << target << " found" << endl;
    }
    
    return 0;
}

Task: Create a vector of 5 doubles. Implement sum calculation using three different methods: (1) index-based loop, (2) iterator-based loop, (3) range-based for loop. Print the sum from each method to verify they match.

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

int main() {
    vector<double> values = {1.5, 2.5, 3.5, 4.5, 5.0};
    
    // Method 1: Index-based loop
    double sum1 = 0.0;
    for (size_t i = 0; i < values.size(); ++i) {
        sum1 += values[i];
    }
    cout << "Index-based sum: " << sum1 << endl;
    
    // Method 2: Iterator-based loop
    double sum2 = 0.0;
    for (auto it = values.begin(); it != values.end(); ++it) {
        sum2 += *it;
    }
    cout << "Iterator-based sum: " << sum2 << endl;
    
    // Method 3: Range-based for loop
    double sum3 = 0.0;
    for (const auto& val : values) {
        sum3 += val;
    }
    cout << "Range-based sum: " << sum3 << endl;
    
    // Verify all match
    cout << "\nAll methods equal: " 
         << ((sum1 == sum2 && sum2 == sum3) ? "Yes" : "No") << endl;
    
    return 0;
}
// Output:
// Index-based sum: 17
// Iterator-based sum: 17
// Range-based sum: 17
// All methods equal: Yes
02

Iterator Categories

Not all iterators are created equal. C++ defines five iterator categories, each with different capabilities. Understanding these categories helps you choose the right algorithms and write more efficient code.

The Iterator Hierarchy

Iterator categories form a hierarchy where each level inherits capabilities from the level below and adds new ones. Random Access iterators can do everything, while Input iterators are the most limited. The category determines which algorithms can use that iterator.

Category Read Write ++ -- +n, -n Compare Example Containers
Input == != istream_iterator
Output - ostream_iterator, back_inserter
Forward == != forward_list, unordered_set
Bidirectional == != list, set, map
Random Access == != < > <= >= vector, deque, array
Input Iterator

Input iterators are read-only and single-pass - you can read each element only once as you move forward. Think of reading from a keyboard or network stream where data comes in sequentially and cannot be re-read. They support *it (read), ++it, and ==/!= comparison.

#include <iostream>
#include <iterator>
#include <sstream>
using namespace std;

int main() {
    // Input iterator from a string stream
    istringstream input("10 20 30 40 50");
    
    istream_iterator<int> it(input);  // Points to first value
    istream_iterator<int> end;         // End-of-stream iterator
    
    cout << "Reading with input iterator: ";
    while (it != end) {
        cout << *it << " ";  // Read value
        ++it;                // Move to next (single-pass!)
    }
    cout << endl;
    // Output: Reading with input iterator: 10 20 30 40 50
    
    return 0;
}

Input iterators are the most restricted type. Once you move past an element with ++it, you cannot go back to read it again. This models streams like keyboard input or file reading. The istream_iterator is a classic example - it reads values from an input stream one at a time.

Output Iterator

Output iterators are write-only and single-pass - you can write to each position only once. They're like writing to a printer or network connection. They support *it = value (write) and ++it, but you cannot read from them or compare them.

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

int main() {
    vector<int> source = {1, 2, 3, 4, 5};
    
    // Output iterator to cout
    cout << "Output to console: ";
    ostream_iterator<int> out(cout, " ");
    
    for (int n : source) {
        *out = n;  // Write value
        ++out;     // Move to next position
    }
    cout << endl;
    // Output: Output to console: 1 2 3 4 5
    
    // back_inserter is also an output iterator
    vector<int> dest;
    copy(source.begin(), source.end(), back_inserter(dest));
    
    cout << "Copied to vector: ";
    for (int n : dest) cout << n << " ";
    cout << endl;
    // Output: Copied to vector: 1 2 3 4 5
    
    return 0;
}

Output iterators write values but cannot read them back. The ostream_iterator writes to output streams, and back_inserter appends to containers. These are commonly used with algorithms like copy() to output results.

Forward Iterator

Forward iterators combine input and output capabilities and allow multiple passes through the data. You can read, write, and traverse forward multiple times. However, you still cannot move backward. forward_list and unordered containers provide forward iterators.

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

int main() {
    forward_list<int> flist = {10, 20, 30, 40, 50};
    
    // Forward iterator - can only move forward
    auto it = flist.begin();
    
    cout << "First pass: ";
    while (it != flist.end()) {
        cout << *it << " ";
        ++it;  // Only ++ is available, not --
    }
    cout << endl;
    // Output: First pass: 10 20 30 40 50
    
    // Multi-pass: can traverse again
    cout << "Second pass: ";
    for (auto it2 = flist.begin(); it2 != flist.end(); ++it2) {
        *it2 *= 2;  // Can read AND write
        cout << *it2 << " ";
    }
    cout << endl;
    // Output: Second pass: 20 40 60 80 100
    
    // Note: it-- would NOT compile for forward_list!
    
    return 0;
}

Forward iterators are multi-pass (unlike input/output iterators) so you can save a copy and revisit elements. They support both reading and writing. The forward_list is a singly-linked list that only supports forward traversal, making it memory-efficient but limited to forward iteration.

Bidirectional Iterator

Bidirectional iterators add backward movement with the -- operator. You can traverse in both directions, which enables algorithms like reverse iteration. list, set, and map provide bidirectional iterators.

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

int main() {
    list<int> numbers = {10, 20, 30, 40, 50};
    
    // Bidirectional - can go forward AND backward
    auto it = numbers.end();  // Past the last element
    
    cout << "Backward traversal: ";
    while (it != numbers.begin()) {
        --it;  // Move backward first (since end() is past-the-end)
        cout << *it << " ";
    }
    cout << endl;
    // Output: Backward traversal: 50 40 30 20 10
    
    // Set also has bidirectional iterators
    set<string> words = {"apple", "banana", "cherry"};
    
    cout << "Set backward: ";
    auto sit = words.end();
    while (sit != words.begin()) {
        --sit;
        cout << *sit << " ";
    }
    cout << endl;
    // Output: Set backward: cherry banana apple
    
    return 0;
}

Bidirectional iterators support --it in addition to ++it. This is necessary for containers like list (doubly-linked) and tree-based containers (set, map). Note the pattern for backward iteration: start at end() and decrement first before dereferencing.

Random Access Iterator

Random access iterators are the most powerful - they support all operations including arithmetic (it + n, it - n), subscript (it[n]), and comparison (<, >, etc.). This enables O(1) jumps to any position. vector, deque, and arrays provide these.

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

int main() {
    vector<int> nums = {10, 20, 30, 40, 50, 60, 70};
    
    auto it = nums.begin();
    
    // Jump forward by n positions
    cout << "it + 3: " << *(it + 3) << endl;      // 40
    
    // Jump backward
    auto it2 = nums.end();
    cout << "end - 2: " << *(it2 - 2) << endl;    // 60
    
    // Subscript access
    cout << "it[4]: " << it[4] << endl;            // 50
    
    // Calculate distance
    auto first = nums.begin();
    auto last = nums.end();
    cout << "Distance: " << (last - first) << endl; // 7
    
    // Comparison operators
    auto mid = nums.begin() + 3;
    cout << "begin < mid: " << (first < mid) << endl;   // 1 (true)
    cout << "mid < end: " << (mid < last) << endl;      // 1 (true)
    
    // Compound assignment
    it += 2;
    cout << "After it += 2: " << *it << endl;     // 30
    it -= 1;
    cout << "After it -= 1: " << *it << endl;     // 20
    
    return 0;
}

Random access iterators behave almost exactly like pointers. You can do pointer arithmetic (it + n), compute distances (it2 - it1), use subscript notation (it[n]), and compare with <, >, etc. This enables efficient algorithms like binary search and random shuffling. Only contiguous containers (vector, deque, array) support this.

Tip: When writing generic code, prefer using the minimum iterator category needed. If your algorithm only needs forward iteration, don't require random access - this lets it work with more container types.

Practice Questions: Iterator Categories

Task: Given a vector and a list, demonstrate which operations work on each. Try: it + 3, it[2], --it, it1 < it2. Show which compile and which don't for each container type.

Show Solution
#include <iostream>
#include <vector>
#include <list>
using namespace std;

int main() {
    vector<int> vec = {10, 20, 30, 40, 50};
    list<int> lst = {10, 20, 30, 40, 50};
    
    auto vit = vec.begin();
    auto lit = lst.begin();
    
    cout << "=== Vector (Random Access Iterator) ===" << endl;
    
    // All these work for vector:
    cout << "*(vit + 3): " << *(vit + 3) << endl;  // 40 - arithmetic
    cout << "vit[2]: " << vit[2] << endl;          // 30 - subscript
    ++vit;
    --vit;                                          // decrement works
    cout << "After ++, --: " << *vit << endl;      // 10
    
    auto vit2 = vec.begin() + 3;
    cout << "vit < vit2: " << (vit < vit2) << endl; // 1 - comparison
    
    cout << "\n=== List (Bidirectional Iterator) ===" << endl;
    
    // These work for list:
    ++lit;
    --lit;                                          // decrement works
    cout << "After ++, --: " << *lit << endl;      // 10
    cout << "lit == lst.begin(): " << (lit == lst.begin()) << endl; // 1
    
    // These DON'T work for list (would not compile):
    // cout << *(lit + 3);     // ERROR: no operator+
    // cout << lit[2];         // ERROR: no operator[]
    // cout << (lit < lit2);   // ERROR: no operator<
    
    cout << "\n(lit + 3, lit[2], and lit < lit2 would NOT compile for list)" << endl;
    
    return 0;
}
// Output:
// === Vector (Random Access Iterator) ===
// *(vit + 3): 40
// vit[2]: 30
// After ++, --: 10
// vit < vit2: 1
// === List (Bidirectional Iterator) ===
// After ++, --: 10
// lit == lst.begin(): 1
// (lit + 3, lit[2], and lit < lit2 would NOT compile for list)

Task: Create a forward_list with 5 integers. Use its forward iterator to: (1) print all elements, (2) double each element in place, (3) print again. Remember that -- is not available.

Show Solution
#include <iostream>
#include <forward_list>
using namespace std;

int main() {
    forward_list<int> flist = {1, 2, 3, 4, 5};
    
    // Print all elements
    cout << "Original: ";
    for (auto it = flist.begin(); it != flist.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;
    // Output: Original: 1 2 3 4 5
    
    // Double each element (forward iterator supports read AND write)
    for (auto it = flist.begin(); it != flist.end(); ++it) {
        *it = *it * 2;
    }
    
    // Print again
    cout << "Doubled: ";
    for (auto it = flist.begin(); it != flist.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;
    // Output: Doubled: 2 4 6 8 10
    
    // Note: This would NOT compile:
    // auto it = flist.end();
    // --it;  // ERROR: forward_list iterators don't support --
    
    cout << "\nForward iterators support ++ but not --" << endl;
    
    return 0;
}

Task: Create a vector with 10 elements (0-9). Using random access iterator arithmetic, print: (1) every other element starting from index 0, (2) elements from index 2 to 7 using iterator subtraction to verify count, (3) the middle element using arithmetic.

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

int main() {
    vector<int> nums = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    
    // 1. Every other element starting from index 0
    cout << "Every other element: ";
    for (auto it = nums.begin(); it < nums.end(); it += 2) {
        cout << *it << " ";
    }
    cout << endl;
    // Output: Every other element: 0 2 4 6 8
    
    // 2. Elements from index 2 to 7 using iterator subtraction
    auto start = nums.begin() + 2;
    auto stop = nums.begin() + 8;  // 8 because end is exclusive
    
    cout << "Elements [2,7]: ";
    for (auto it = start; it < stop; ++it) {
        cout << *it << " ";
    }
    cout << endl;
    cout << "Count (using subtraction): " << (stop - start) << endl;
    // Output: Elements [2,7]: 2 3 4 5 6 7
    // Count (using subtraction): 6
    
    // 3. Middle element using arithmetic
    auto mid = nums.begin() + nums.size() / 2;
    cout << "Middle element (index " << (mid - nums.begin()) << "): " << *mid << endl;
    // Output: Middle element (index 5): 5
    
    // Alternative: using distance from both ends
    auto middle = nums.begin() + (nums.end() - nums.begin()) / 2;
    cout << "Middle (alternative): " << *middle << endl;
    
    return 0;
}

Task: Create a list of strings {"one", "two", "three", "four", "five"}. Using bidirectional iterator with --, traverse the list backward and print each element. Then traverse forward again and print the last 3 elements only.

Show Solution
#include <iostream>
#include <list>
#include <string>
using namespace std;

int main() {
    list<string> words = {"one", "two", "three", "four", "five"};
    
    // Backward traversal using --
    cout << "Backward: ";
    auto it = words.end();
    while (it != words.begin()) {
        --it;  // Decrement first since end() is past-the-end
        cout << *it << " ";
    }
    cout << endl;
    // Output: Backward: five four three two one
    
    // Forward traversal - last 3 elements only
    // For bidirectional iterators, we need to manually advance
    cout << "Last 3 elements: ";
    
    // Start at end and go back 3 positions
    it = words.end();
    --it; --it; --it;  // Now at "three"
    
    while (it != words.end()) {
        cout << *it << " ";
        ++it;
    }
    cout << endl;
    // Output: Last 3 elements: three four five
    
    // Alternative using advance (works with any iterator category)
    cout << "Using advance: ";
    it = words.begin();
    advance(it, 2);  // Move to index 2 ("three")
    while (it != words.end()) {
        cout << *it << " ";
        ++it;
    }
    cout << endl;
    // Output: Using advance: three four five
    
    return 0;
}
03

Iterator Operations

The STL provides utility functions for working with iterators that make your code cleaner and more portable. Functions like advance(), distance(), next(), and prev() work with any iterator type and optimize based on the iterator's capabilities.

std::distance() - Count Elements

The std::distance(first, last) function returns the number of increments needed to get from first to last. For random access iterators, this is O(1) using subtraction. For other iterators, it's O(n) requiring actual iteration.

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

int main() {
    vector<int> vec = {10, 20, 30, 40, 50};
    list<int> lst = {10, 20, 30, 40, 50};
    
    // For vector: O(1) - uses subtraction internally
    auto dist1 = distance(vec.begin(), vec.end());
    cout << "Vector distance: " << dist1 << endl;  // 5
    
    // For list: O(n) - must count each increment
    auto dist2 = distance(lst.begin(), lst.end());
    cout << "List distance: " << dist2 << endl;    // 5
    
    // Distance to specific position
    auto it = vec.begin();
    advance(it, 3);
    cout << "Distance from begin to it: " << distance(vec.begin(), it) << endl;  // 3
    
    return 0;
}
Important: The first iterator must be reachable from last by incrementing. If first comes after last, the behavior is undefined for non-random-access iterators. For random access iterators, you can get negative distances.
std::advance() - Move Iterator

The std::advance(it, n) function moves an iterator n positions. Unlike it + n, this works with all forward iterators, not just random access. For random access iterators, it's O(1). For bidirectional, it can go backward with negative n. For forward, only positive n works.

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

int main() {
    vector<int> vec = {10, 20, 30, 40, 50, 60, 70};
    list<int> lst = {10, 20, 30, 40, 50, 60, 70};
    
    // Advance on vector (random access) - O(1)
    auto vit = vec.begin();
    advance(vit, 3);
    cout << "Vector after advance(3): " << *vit << endl;  // 40
    
    // Advance on list (bidirectional) - O(n)
    auto lit = lst.begin();
    advance(lit, 3);
    cout << "List after advance(3): " << *lit << endl;    // 40
    
    // Negative advance (works for bidirectional and random access)
    advance(vit, -2);
    cout << "Vector after advance(-2): " << *vit << endl; // 20
    
    advance(lit, -2);
    cout << "List after advance(-2): " << *lit << endl;   // 20
    
    // Note: advance modifies the iterator in place (returns void)
    
    return 0;
}

Note that advance() modifies the iterator in place and returns void. If you need a new iterator without modifying the original, use std::next() or std::prev() instead.

std::next() and std::prev() - Get Adjacent Iterator

C++11 introduced std::next(it, n) and std::prev(it, n) which return a new iterator without modifying the original. The default for n is 1. These are safer and more readable than advance() when you need the original iterator preserved.

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

int main() {
    vector<int> vec = {10, 20, 30, 40, 50};
    
    auto it = vec.begin();
    
    // next() returns a new iterator, original unchanged
    auto it2 = next(it);      // Move 1 forward (default)
    auto it3 = next(it, 3);   // Move 3 forward
    
    cout << "*it: " << *it << endl;   // 10 (unchanged!)
    cout << "*it2: " << *it2 << endl; // 20
    cout << "*it3: " << *it3 << endl; // 40
    
    // prev() moves backward
    auto it4 = prev(vec.end());       // Last element
    auto it5 = prev(vec.end(), 2);    // Second to last
    
    cout << "*it4: " << *it4 << endl; // 50
    cout << "*it5: " << *it5 << endl; // 40
    
    // Useful pattern: iterate over all but first/last
    cout << "All but first and last: ";
    for (auto iter = next(vec.begin()); iter != prev(vec.end()); ++iter) {
        cout << *iter << " ";
    }
    cout << endl;  // 20 30 40
    
    return 0;
}
advance() vs next()/prev()
  • advance(it, n): Modifies it in place, returns void
  • next(it, n): Returns new iterator, it unchanged
  • prev(it, n): Returns new iterator, it unchanged
  • Use next/prev for single expressions
  • Use advance when modifying loop iterators
Performance by Iterator Type
  • Random Access: All operations O(1)
  • Bidirectional: O(n) for distance/advance
  • Forward: O(n), positive only
  • Input/Output: Limited support
  • Compiler picks optimal implementation automatically
std::begin() and std::end() - Unified Interface

The free functions std::begin() and std::end() (C++11) provide a unified way to get iterators from both STL containers and raw arrays. This enables writing generic code that works with any iterable type.

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

// Generic function that works with ANY iterable
template<typename Container>
void printAll(const Container& c) {
    for (auto it = begin(c); it != end(c); ++it) {
        cout << *it << " ";
    }
    cout << endl;
}

int main() {
    // Works with STL containers
    vector<int> vec = {1, 2, 3, 4, 5};
    printAll(vec);  // 1 2 3 4 5
    
    // Works with raw arrays!
    int arr[] = {10, 20, 30, 40, 50};
    printAll(arr);  // 10 20 30 40 50
    
    // Use with algorithms
    sort(begin(arr), end(arr), greater<int>());
    printAll(arr);  // 50 40 30 20 10
    
    // Also: cbegin/cend for const, rbegin/rend for reverse
    cout << "Reverse: ";
    for (auto it = rbegin(vec); it != rend(vec); ++it) {
        cout << *it << " ";
    }
    cout << endl;  // 5 4 3 2 1
    
    return 0;
}

Always prefer std::begin()/std::end() in generic code. For C++14 and later, you also have std::cbegin()/std::cend() (const) and std::rbegin()/std::rend() (reverse). C++17 adds std::size(), std::empty(), and std::data().

Practice Questions: Iterator Operations

Task: Create a vector with elements 0-9. Use std::distance() to find how many elements are between index 2 and index 7 (inclusive). Then use std::advance() to move an iterator to the middle element and print it.

Show Solution
#include <iostream>
#include <iterator>
#include <vector>
using namespace std;

int main() {
    vector<int> nums = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    
    // Find distance between index 2 and index 7 (inclusive)
    auto pos2 = nums.begin() + 2;  // Points to element 2
    auto pos7 = nums.begin() + 8;  // Points past element 7 (for inclusive range)
    
    auto count = distance(pos2, pos7);
    cout << "Elements from index 2 to 7 (inclusive): " << count << endl;
    // Output: Elements from index 2 to 7 (inclusive): 6
    
    // Move iterator to middle element
    auto it = nums.begin();
    advance(it, nums.size() / 2);
    
    cout << "Middle element (index " << distance(nums.begin(), it) << "): " << *it << endl;
    // Output: Middle element (index 5): 5
    
    return 0;
}

Task: Create a list with strings {"A", "B", "C", "D", "E"}. Using std::next() and std::prev(), print: (1) the second element, (2) the second-to-last element, (3) elements B, C, D (skip first and last).

Show Solution
#include <iostream>
#include <iterator>
#include <list>
#include <string>
using namespace std;

int main() {
    list<string> letters = {"A", "B", "C", "D", "E"};
    
    // 1. Second element using next()
    auto second = next(letters.begin());
    cout << "Second element: " << *second << endl;
    // Output: Second element: B
    
    // 2. Second-to-last element using prev()
    auto secondLast = prev(letters.end(), 2);
    cout << "Second-to-last: " << *secondLast << endl;
    // Output: Second-to-last: D
    
    // 3. Elements B, C, D (skip first and last)
    cout << "Middle elements: ";
    for (auto it = next(letters.begin()); it != prev(letters.end()); ++it) {
        cout << *it << " ";
    }
    cout << endl;
    // Output: Middle elements: B C D
    
    return 0;
}

Task: Write a template function findMax() that works with both STL containers and raw arrays using std::begin()/std::end(). Test it with a vector and a raw int array.

Show Solution
#include <iostream>
#include <iterator>
#include <vector>
using namespace std;

template<typename Container>
auto findMax(const Container& c) {
    auto maxIt = begin(c);
    for (auto it = begin(c); it != end(c); ++it) {
        if (*it > *maxIt) {
            maxIt = it;
        }
    }
    return *maxIt;
}

int main() {
    // Test with vector
    vector<int> vec = {34, 67, 23, 89, 45, 12};
    cout << "Vector max: " << findMax(vec) << endl;
    // Output: Vector max: 89
    
    // Test with raw array
    int arr[] = {15, 82, 43, 91, 27, 56};
    cout << "Array max: " << findMax(arr) << endl;
    // Output: Array max: 91
    
    // Test with different type
    double doubles[] = {3.14, 2.71, 1.41, 1.73};
    cout << "Double array max: " << findMax(doubles) << endl;
    // Output: Double array max: 3.14
    
    return 0;
}

Task: Implement a function that calculates the maximum sum of any 3 consecutive elements in a vector using iterator operations (advance, distance, next). Return both the maximum sum and the starting index.

Show Solution
#include <iostream>
#include <iterator>
#include <vector>
using namespace std;

pair<int, size_t> maxWindowSum(const vector<int>& nums, int windowSize) {
    if (distance(nums.begin(), nums.end()) < windowSize) {
        return {0, 0};  // Not enough elements
    }
    
    int maxSum = 0;
    size_t maxIndex = 0;
    
    // Calculate initial window sum
    int currentSum = 0;
    auto windowEnd = nums.begin();
    advance(windowEnd, windowSize);
    for (auto it = nums.begin(); it != windowEnd; ++it) {
        currentSum += *it;
    }
    maxSum = currentSum;
    
    // Slide the window
    auto left = nums.begin();
    auto right = windowEnd;
    
    while (right != nums.end()) {
        currentSum -= *left;  // Remove leftmost
        currentSum += *right; // Add rightmost
        
        if (currentSum > maxSum) {
            maxSum = currentSum;
            maxIndex = distance(nums.begin(), next(left));
        }
        
        ++left;
        ++right;
    }
    
    return {maxSum, maxIndex};
}

int main() {
    vector<int> nums = {2, 1, 5, 1, 3, 2, 8, 1, 4};
    
    auto [maxSum, startIdx] = maxWindowSum(nums, 3);
    
    cout << "Max sum of 3 consecutive: " << maxSum << endl;
    cout << "Starting at index: " << startIdx << endl;
    cout << "Elements: ";
    for (int i = 0; i < 3; ++i) {
        cout << nums[startIdx + i] << " ";
    }
    cout << endl;
    // Output:
    // Max sum of 3 consecutive: 13
    // Starting at index: 5
    // Elements: 2 8 1
    
    return 0;
}
04

Special Iterators

Beyond regular iterators, C++ provides specialized iterator types for specific use cases. Reverse iterators traverse containers backward, const iterators prevent modification, and stream iterators connect containers to I/O streams.

Reverse Iterators

Reverse iterators traverse containers from end to beginning. Every container with bidirectional iterators provides rbegin() and rend(). The rbegin() points to the last element, and rend() points before the first element.

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

int main() {
    vector<int> nums = {10, 20, 30, 40, 50};
    
    // Reverse iteration with rbegin/rend
    cout << "Reverse: ";
    for (auto rit = nums.rbegin(); rit != nums.rend(); ++rit) {
        cout << *rit << " ";  // Note: ++ moves backward!
    }
    cout << endl;  // 50 40 30 20 10
    
    // Range-based for with reverse adaptor (C++20)
    // for (int n : nums | views::reverse) { ... }
    
    // Find last occurrence using reverse iterator
    auto it = find(nums.rbegin(), nums.rend(), 30);
    if (it != nums.rend()) {
        cout << "Found 30 at reverse position: " << distance(nums.rbegin(), it) << endl;
    }
    
    // Convert reverse iterator to regular iterator using base()
    auto regular = it.base();  // Points to element AFTER the one rit points to
    cout << "Element after 30: " << *regular << endl;  // 40
    
    return 0;
}
Note on base(): When converting a reverse iterator to a regular iterator with base(), the result points to the element after the one the reverse iterator points to. This is due to how reverse iterators work internally.
Const Iterators

Const iterators (const_iterator) allow reading elements but prevent modification. Use cbegin()/cend() to get const iterators. This enforces const-correctness and signals intent to readers.

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

void printVector(const vector<int>& v) {
    // On const containers, begin() returns const_iterator automatically
    for (auto it = v.begin(); it != v.end(); ++it) {
        cout << *it << " ";
        // *it = 100;  // ERROR: cannot modify through const_iterator
    }
    cout << endl;
}

int main() {
    vector<int> nums = {1, 2, 3, 4, 5};
    
    // Explicitly get const iterators with cbegin/cend
    for (auto it = nums.cbegin(); it != nums.cend(); ++it) {
        cout << *it << " ";
        // *it = 10;  // ERROR: const_iterator prevents modification
    }
    cout << endl;
    
    // Regular iterator - can modify
    for (auto it = nums.begin(); it != nums.end(); ++it) {
        *it *= 2;  // OK: regular iterator allows modification
    }
    
    printVector(nums);  // 2 4 6 8 10
    
    return 0;
}

Best practice: Use cbegin()/cend() when you don't need to modify elements. This makes your intent clear and can catch accidental modifications at compile time.

Stream Iterators

Stream iterators connect I/O streams to the iterator interface. istream_iterator reads from input streams, and ostream_iterator writes to output streams. This enables using algorithms directly with streams.

#include <iostream>
#include <iterator>
#include <vector>
#include <algorithm>
#include <sstream>
using namespace std;

int main() {
    // Read integers from string stream
    istringstream input("10 20 30 40 50");
    vector<int> nums;
    
    // Copy from stream to vector using istream_iterator
    copy(istream_iterator<int>(input),
         istream_iterator<int>(),  // End-of-stream (default constructed)
         back_inserter(nums));
    
    cout << "Read from stream: ";
    for (int n : nums) cout << n << " ";
    cout << endl;  // 10 20 30 40 50
    
    // Write to cout using ostream_iterator
    cout << "Output with separator: ";
    copy(nums.begin(), nums.end(), ostream_iterator<int>(cout, ", "));
    cout << endl;  // 10, 20, 30, 40, 50,
    
    // Transform and output
    cout << "Doubled: ";
    transform(nums.begin(), nums.end(),
              ostream_iterator<int>(cout, " "),
              [](int x) { return x * 2; });
    cout << endl;  // 20 40 60 80 100
    
    return 0;
}
Insert Iterators

Insert iterators automatically grow containers when assigned to. back_inserter appends to the end, front_inserter prepends to the front, and inserter inserts at a specified position.

#include <iostream>
#include <iterator>
#include <vector>
#include <list>
#include <deque>
#include <algorithm>
using namespace std;

int main() {
    vector<int> source = {1, 2, 3, 4, 5};
    
    // back_inserter: append to end
    vector<int> v1;
    copy(source.begin(), source.end(), back_inserter(v1));
    cout << "back_inserter: ";
    for (int n : v1) cout << n << " ";
    cout << endl;  // 1 2 3 4 5
    
    // front_inserter: prepend to front (reverses order!)
    list<int> lst;  // Note: vector doesn't support push_front
    copy(source.begin(), source.end(), front_inserter(lst));
    cout << "front_inserter: ";
    for (int n : lst) cout << n << " ";
    cout << endl;  // 5 4 3 2 1
    
    // inserter: insert at position
    vector<int> v2 = {100, 200};
    copy(source.begin(), source.end(), inserter(v2, v2.begin() + 1));
    cout << "inserter at pos 1: ";
    for (int n : v2) cout << n << " ";
    cout << endl;  // 100 1 2 3 4 5 200
    
    return 0;
}

Insert iterators are essential when using algorithms that write output. Without them, you would need to pre-allocate space in the destination container. Note that front_inserter reverses the order because each element is inserted at the front.

Practice Questions: Special Iterators

Task: Create a vector with strings {"one", "two", "three", "four", "five"}. Use reverse iterators to print them in reverse order. Then find "three" using reverse iterators and print its position from the end.

Show Solution
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;

int main() {
    vector<string> words = {"one", "two", "three", "four", "five"};
    
    // Print in reverse order
    cout << "Reversed: ";
    for (auto rit = words.rbegin(); rit != words.rend(); ++rit) {
        cout << *rit << " ";
    }
    cout << endl;
    // Output: Reversed: five four three two one
    
    // Find "three" using reverse iterator
    auto found = find(words.rbegin(), words.rend(), "three");
    
    if (found != words.rend()) {
        auto posFromEnd = distance(words.rbegin(), found);
        cout << "Found 'three' at position " << posFromEnd << " from end" << endl;
        // Output: Found 'three' at position 2 from end
    }
    
    return 0;
}

Task: Write a function sumElements() that takes a vector by const reference and uses cbegin()/cend() to calculate the sum. This ensures elements cannot be modified.

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

// Using cbegin/cend ensures we don't modify elements
int sumElements(const vector<int>& nums) {
    int sum = 0;
    for (auto it = nums.cbegin(); it != nums.cend(); ++it) {
        sum += *it;
        // *it = 0;  // Would NOT compile - const_iterator prevents modification
    }
    return sum;
}

int main() {
    vector<int> numbers = {10, 20, 30, 40, 50};
    
    cout << "Sum: " << sumElements(numbers) << endl;
    // Output: Sum: 150
    
    // Original vector unchanged (guaranteed by const_iterator)
    cout << "First element still: " << numbers[0] << endl;
    // Output: First element still: 10
    
    return 0;
}

Task: Read space-separated doubles from a string stream "3.14 2.71 1.41 1.73" using istream_iterator. Square each number and output the results using ostream_iterator with " | " as separator.

Show Solution
#include <iostream>
#include <iterator>
#include <vector>
#include <algorithm>
#include <sstream>
#include <cmath>
using namespace std;

int main() {
    istringstream input("3.14 2.71 1.41 1.73");
    vector<double> nums;
    
    // Read from stream
    copy(istream_iterator<double>(input),
         istream_iterator<double>(),
         back_inserter(nums));
    
    cout << "Original: ";
    copy(nums.begin(), nums.end(), ostream_iterator<double>(cout, " "));
    cout << endl;
    // Output: Original: 3.14 2.71 1.41 1.73
    
    // Square each and output with separator
    cout << "Squared: ";
    transform(nums.begin(), nums.end(),
              ostream_iterator<double>(cout, " | "),
              [](double x) { return x * x; });
    cout << endl;
    // Output: Squared: 9.8596 | 7.3441 | 1.9881 | 2.9929 |
    
    return 0;
}

Task: Create a source vector {1,2,3,4,5}. Using copy_if with back_inserter, copy only even numbers to a new vector. Then use transform with back_inserter to create a third vector with all numbers tripled.

Show Solution
#include <iostream>
#include <iterator>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    vector<int> source = {1, 2, 3, 4, 5};
    
    // Copy only even numbers
    vector<int> evens;
    copy_if(source.begin(), source.end(),
            back_inserter(evens),
            [](int x) { return x % 2 == 0; });
    
    cout << "Evens: ";
    for (int n : evens) cout << n << " ";
    cout << endl;
    // Output: Evens: 2 4
    
    // Triple all numbers
    vector<int> tripled;
    transform(source.begin(), source.end(),
              back_inserter(tripled),
              [](int x) { return x * 3; });
    
    cout << "Tripled: ";
    for (int n : tripled) cout << n << " ";
    cout << endl;
    // Output: Tripled: 3 6 9 12 15
    
    return 0;
}
05

Iterator Best Practices

Using iterators correctly is essential for writing safe, efficient, and maintainable C++ code. This section covers common pitfalls, iterator invalidation rules, and modern C++ iterator patterns that will make you a better programmer.

Iterator Invalidation

Iterator invalidation is one of the most dangerous pitfalls in C++. When a container is modified, some or all iterators pointing into it may become invalid. Using an invalid iterator leads to undefined behavior (crashes, data corruption, or seemingly random errors).

Container Insert Erase
vector All iterators if reallocation; else iterators at/after insertion point Iterators at/after erased element
deque All iterators (unless at front/back) All iterators (unless at front/back)
list No iterators invalidated Only iterators to erased element
set/map No iterators invalidated Only iterators to erased element
unordered_set/map All iterators if rehash; else none Only iterators to erased element
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> nums = {1, 2, 3, 4, 5};
    
    // DANGEROUS: iterator invalidation
    auto it = nums.begin() + 2;  // Points to 3
    cout << "Before push_back: " << *it << endl;  // 3
    
    nums.push_back(6);  // May reallocate!
    // cout << *it;     // UNDEFINED BEHAVIOR - it may be invalid!
    
    // SAFE: Re-acquire iterator after modification
    it = nums.begin() + 2;
    cout << "After re-acquiring: " << *it << endl;  // 3
    
    // COMMON BUG: Erase in loop
    // for (auto it = nums.begin(); it != nums.end(); ++it) {
    //     if (*it % 2 == 0) nums.erase(it);  // BUG: it invalidated!
    // }
    
    // CORRECT: Use return value of erase
    for (auto it = nums.begin(); it != nums.end(); ) {
        if (*it % 2 == 0) {
            it = nums.erase(it);  // erase returns iterator to next element
        } else {
            ++it;
        }
    }
    
    cout << "After removing evens: ";
    for (int n : nums) cout << n << " ";
    cout << endl;  // 1 3 5
    
    return 0;
}
Common Bug: Never store iterators across operations that may invalidate them. Always re-acquire iterators after modifying containers, or use the return value of modifying operations like erase().
Modern C++ Iterator Patterns

Modern C++ (C++11 and later) provides cleaner ways to work with iterators. Range-based for loops, auto type deduction, and structured bindings make code more readable and less error-prone.

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

int main() {
    vector<int> nums = {10, 20, 30, 40, 50};
    map<string, int> scores = {{"Alice", 95}, {"Bob", 87}, {"Carol", 92}};
    
    // Old style (verbose)
    for (vector<int>::iterator it = nums.begin(); it != nums.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;
    
    // Modern: auto (C++11)
    for (auto it = nums.begin(); it != nums.end(); ++it) {
        *it *= 2;  // Can modify
    }
    
    // Modern: range-based for (C++11) - preferred for simple iteration
    for (const auto& n : nums) {  // const& prevents copy and modification
        cout << n << " ";
    }
    cout << endl;  // 20 40 60 80 100
    
    // Structured bindings with maps (C++17)
    for (const auto& [name, score] : scores) {
        cout << name << ": " << score << endl;
    }
    
    // When you need the iterator (index, erase, etc.), use traditional loop
    for (auto it = nums.begin(); it != nums.end(); ++it) {
        cout << "Index " << distance(nums.begin(), it) << ": " << *it << endl;
    }
    
    return 0;
}
Do's
  • Use auto for iterator types
  • Prefer range-based for when possible
  • Use cbegin()/cend() for read-only iteration
  • Re-acquire iterators after container modifications
  • Use erase() return value in loops
  • Check iterator validity before dereferencing
Don'ts
  • Don't dereference end() iterator
  • Don't store iterators across modifications
  • Don't use ++it after erase(it)
  • Don't compare iterators from different containers
  • Don't use invalidated iterators
  • Don't assume iterator validity after push_back
Performance Considerations

Choosing the right iterator operations and patterns can significantly impact performance. Understanding time complexity and avoiding unnecessary copies is essential.

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

int main() {
    vector<int> vec(1000000);
    list<int> lst(1000000);
    
    // O(1) for vector, O(n) for list
    auto vec_mid = vec.begin();
    advance(vec_mid, 500000);  // Fast for vector
    
    auto lst_mid = lst.begin();
    advance(lst_mid, 500000);  // Slow for list - must traverse
    
    // PREFER: Use container-specific methods when available
    // vec.erase(vec.begin()) is O(n) - shifts all elements
    // lst.erase(lst.begin()) is O(1) - just relinks nodes
    
    // EFFICIENT: Reserve capacity to avoid reallocation
    vector<int> nums;
    nums.reserve(1000);  // Pre-allocate
    for (int i = 0; i < 1000; ++i) {
        nums.push_back(i);  // No reallocation, iterators stay valid
    }
    
    // EFFICIENT: Use emplace instead of insert for objects
    vector<pair<int, string>> pairs;
    pairs.emplace_back(1, "one");  // Constructs in place
    // vs pairs.push_back(make_pair(1, "one")); // Creates temporary
    
    cout << "Optimization examples complete" << endl;
    
    return 0;
}
Performance Tip: For large containers, prefer algorithms with lower complexity. Use reserve() for vectors when size is known. Choose containers based on your access patterns - vector for random access, list for frequent insertions/deletions in the middle.

Practice Questions: Best Practices

Task: The following code has an iterator invalidation bug. Fix it so it correctly removes all elements greater than 50 from the vector.

vector<int> nums = {30, 60, 20, 80, 40, 70, 10};
for (auto it = nums.begin(); it != nums.end(); ++it) {
    if (*it > 50) {
        nums.erase(it);  // BUG!
    }
}
Show Solution
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> nums = {30, 60, 20, 80, 40, 70, 10};
    
    // FIXED: Use erase return value, don't increment when erasing
    for (auto it = nums.begin(); it != nums.end(); ) {
        if (*it > 50) {
            it = nums.erase(it);  // Returns iterator to next element
        } else {
            ++it;  // Only increment if we didn't erase
        }
    }
    
    cout << "After removing > 50: ";
    for (int n : nums) cout << n << " ";
    cout << endl;
    // Output: After removing > 50: 30 20 40 10
    
    // Alternative using erase-remove idiom (more idiomatic)
    vector<int> nums2 = {30, 60, 20, 80, 40, 70, 10};
    nums2.erase(
        remove_if(nums2.begin(), nums2.end(), [](int x) { return x > 50; }),
        nums2.end()
    );
    
    cout << "Using erase-remove: ";
    for (int n : nums2) cout << n << " ";
    cout << endl;
    // Output: Using erase-remove: 30 20 40 10
    
    return 0;
}

Task: Rewrite the following old-style loop using modern C++ patterns. Use auto and range-based for where appropriate.

map<string, int> ages;
ages["Alice"] = 25; ages["Bob"] = 30; ages["Carol"] = 28;
for (map<string, int>::iterator it = ages.begin(); it != ages.end(); ++it) {
    cout << (*it).first << " is " << (*it).second << " years old" << endl;
}
Show Solution
#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
    map<string, int> ages = {{"Alice", 25}, {"Bob", 30}, {"Carol", 28}};
    
    // Modern C++11: range-based for with auto
    cout << "C++11 style:" << endl;
    for (const auto& pair : ages) {
        cout << pair.first << " is " << pair.second << " years old" << endl;
    }
    
    // Modern C++17: structured bindings
    cout << "\nC++17 style:" << endl;
    for (const auto& [name, age] : ages) {
        cout << name << " is " << age << " years old" << endl;
    }
    
    // When modification is needed
    cout << "\nAfter birthday:" << endl;
    for (auto& [name, age] : ages) {
        age++;  // Modify through reference
    }
    for (const auto& [name, age] : ages) {
        cout << name << " is " << age << " years old" << endl;
    }
    
    return 0;
}

Task: Write a function that takes a vector<int> and doubles all positive numbers while removing all negative numbers, safely handling iterator invalidation.

Show Solution
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

void processNumbers(vector<int>& nums) {
    // Method 1: Two-pass approach (safer, cleaner)
    // First remove negatives, then double positives
    
    // Remove negatives using erase-remove
    nums.erase(
        remove_if(nums.begin(), nums.end(), [](int x) { return x < 0; }),
        nums.end()
    );
    
    // Double all remaining (positive) numbers
    for (int& n : nums) {
        n *= 2;
    }
}

void processNumbersSinglePass(vector<int>& nums) {
    // Method 2: Single pass with careful iterator handling
    for (auto it = nums.begin(); it != nums.end(); ) {
        if (*it < 0) {
            it = nums.erase(it);  // Remove negative
        } else {
            *it *= 2;  // Double positive
            ++it;
        }
    }
}

int main() {
    vector<int> nums1 = {-5, 10, -3, 20, 0, -8, 15};
    vector<int> nums2 = nums1;  // Copy for second test
    
    processNumbers(nums1);
    cout << "Two-pass result: ";
    for (int n : nums1) cout << n << " ";
    cout << endl;
    // Output: Two-pass result: 20 40 0 30
    
    processNumbersSinglePass(nums2);
    cout << "Single-pass result: ";
    for (int n : nums2) cout << n << " ";
    cout << endl;
    // Output: Single-pass result: 20 40 0 30
    
    return 0;
}

Task: You have a large dataset of 10000 Person objects. Write efficient code to: (1) reserve space, (2) populate with emplace_back, (3) find all adults (age >= 18) efficiently using iterators, (4) count them without creating a new container.

Show Solution
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;

struct Person {
    string name;
    int age;
    
    Person(const string& n, int a) : name(n), age(a) {}
};

int main() {
    const int SIZE = 10000;
    
    // 1. Reserve space to avoid reallocations
    vector<Person> people;
    people.reserve(SIZE);  // No iterator invalidation during population
    
    // 2. Populate efficiently with emplace_back
    for (int i = 0; i < SIZE; ++i) {
        people.emplace_back("Person" + to_string(i), i % 100);
    }
    
    // 3 & 4. Count adults efficiently without creating new container
    auto adultCount = count_if(people.cbegin(), people.cend(),
                               [](const Person& p) { return p.age >= 18; });
    
    cout << "Total people: " << people.size() << endl;
    cout << "Adults (age >= 18): " << adultCount << endl;
    
    // If we need to iterate over adults without copying:
    cout << "\nFirst 5 adults:" << endl;
    int printed = 0;
    for (auto it = people.cbegin(); it != people.cend() && printed < 5; ++it) {
        if (it->age >= 18) {
            cout << it->name << " (" << it->age << ")" << endl;
            ++printed;
        }
    }
    
    // Alternative: partition to group adults at beginning (modifies order)
    // auto adultEnd = partition(people.begin(), people.end(),
    //                          [](const Person& p) { return p.age >= 18; });
    // Now [begin, adultEnd) are adults, [adultEnd, end) are minors
    
    return 0;
}

Interactive: Iterator Visualizer

Explore how iterators move through containers. Click the buttons to see iterator operations in action.

Iterator Position Demo
begin()
10 [0]
20 [1]
30 [2]
40 [3]
50 [4]
end()
it = begin()
*it = 10
Click buttons to move the iterator through the container.
Iterator Category Chooser

Select the operations you need, and find out which iterator category is required:

Minimum Required Category:

Input Iterator

Works with: istream_iterator

Key Takeaways

Generalized Pointers

Iterators act like smart pointers that know how to traverse their container, abstracting away implementation details.

Five Categories

Input, Output, Forward, Bidirectional, and Random Access - each with increasing capabilities.

begin() and end()

Every container provides begin() and end() iterators defining a half-open range [begin, end).

Reverse Iteration

Use rbegin() and rend() for backward traversal without changing your loop structure.

Const Correctness

Use cbegin() and cend() when you don't need to modify elements for safety.

Invalidation Rules

Know when operations invalidate iterators to avoid undefined behavior and crashes.

Knowledge Check

Test your understanding of C++ STL Iterators:

Question 1 of 6

What does end() return for a container?

Question 2 of 6

Which iterator category supports random access (jumping to any position)?

Question 3 of 6

What function moves an iterator by a specified number of positions?

Question 4 of 6

What happens when you dereference end()?

Question 5 of 6

Which iterator type should you use to traverse a container backward?

Question 6 of 6

What does iterator invalidation mean?