Iterators#

Source:

src/rust/iterators

Rust iterators are lazy and chainable, similar to C++20 ranges. Operations like map and filter don’t execute until you consume the iterator (e.g., with collect). This lazy evaluation allows the compiler to fuse multiple operations into a single pass over the data, often generating code as efficient as hand-written loops. Unlike C++ STL algorithms which operate on iterator pairs, Rust iterators are single objects that carry their own state, making them easier to compose and pass around. The iterator trait system also enables powerful abstractions - any type that implements Iterator automatically gains access to dozens of adapter methods.

Basic Iteration#

Rust provides multiple ways to iterate over collections, each with different ownership semantics. The for loop syntax automatically calls into_iter() on the collection, but you can control borrowing behavior with & and &mut. This example shows the basic iteration patterns in both languages:

C++:

#include <iostream>
#include <vector>

int main() {
  std::vector<int> v = {1, 2, 3, 4, 5};

  // Range-based for (copies by default, use & for reference)
  std::cout << "By value: ";
  for (int x : v) {
    std::cout << x << " ";
  }
  std::cout << "\n";

  // By reference (no copy)
  std::cout << "By reference: ";
  for (const int& x : v) {
    std::cout << x << " ";
  }
  std::cout << "\n";

  // Iterator-based (traditional style)
  std::cout << "Iterator-based: ";
  for (auto it = v.begin(); it != v.end(); ++it) {
    std::cout << *it << " ";
  }
  std::cout << "\n";

  return 0;
}

Rust:

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    // Borrowing iteration - v remains valid after loop
    print!("By reference: ");
    for x in &v {
        print!("{} ", x);  // x is &i32
    }
    println!();

    // v is still valid here
    println!("v still has {} elements", v.len());

    // Consuming iteration - v is moved into the loop
    print!("By value (consuming): ");
    for x in v {
        print!("{} ", x);  // x is i32
    }
    println!();

    // v is no longer valid here - would be compile error to use it
    // println!("{}", v.len());  // error: borrow of moved value
}

Three Iterator Types#

Rust collections provide three iterator methods that control ownership: iter() borrows elements immutably, iter_mut() borrows mutably, and into_iter() takes ownership. Understanding these is crucial for writing efficient Rust code. C++ achieves similar patterns through const/non-const iterators and move semantics:

C++:

#include <iostream>
#include <vector>

int main() {
  std::vector<int> v = {1, 2, 3};

  // Const iteration (like Rust's iter())
  std::cout << "Const iteration: ";
  for (auto it = v.cbegin(); it != v.cend(); ++it) {
    std::cout << *it << " ";
    // *it = 10;  // error: cannot modify through const iterator
  }
  std::cout << "\n";

  // Mutable iteration (like Rust's iter_mut())
  std::cout << "Mutable iteration: ";
  for (auto it = v.begin(); it != v.end(); ++it) {
    *it += 10;  // can modify
    std::cout << *it << " ";
  }
  std::cout << "\n";

  // Move iteration (like Rust's into_iter())
  std::vector<std::string> strings = {"hello", "world"};
  std::vector<std::string> moved;
  for (auto& s : strings) {
    moved.push_back(std::move(s));  // explicit move required
  }
  // strings still exists but elements are in moved-from state

  return 0;
}

Rust:

fn main() {
    let mut v = vec![1, 2, 3];

    // iter() - borrows immutably, yields &T
    print!("iter() yields &T: ");
    for x in v.iter() {
        print!("{} ", x);  // x is &i32
        // *x = 10;  // error: cannot assign to immutable reference
    }
    println!();

    // iter_mut() - borrows mutably, yields &mut T
    print!("iter_mut() yields &mut T: ");
    for x in v.iter_mut() {
        *x += 10;  // can modify through mutable reference
        print!("{} ", x);
    }
    println!();

    // into_iter() - consumes collection, yields T
    print!("into_iter() yields T: ");
    for x in v.into_iter() {
        print!("{} ", x);  // x is i32, owns the value
    }
    println!();

    // v is no longer valid - ownership was transferred
    // println!("{:?}", v);  // error: borrow of moved value
}

Iterator Adapters#

Iterator adapters transform iterators without consuming them. They’re lazy - no work happens until you call a consuming method like collect(). This enables efficient chaining of multiple operations. Each adapter below shows both C++ and Rust approaches.

map - Transform Elements#

The map adapter applies a function to each element, producing a new iterator of transformed values. This is equivalent to C++’s std::transform:

C++:

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

int main() {
  std::vector<int> v = {1, 2, 3};
  std::vector<int> doubled;

  // std::transform requires output iterator
  std::transform(v.begin(), v.end(), std::back_inserter(doubled),
                 [](int x) { return x * 2; });

  std::cout << "Doubled: ";
  for (int x : doubled) {
    std::cout << x << " ";  // 2 4 6
  }
  std::cout << "\n";

  return 0;
}

Rust:

fn main() {
    let v = vec![1, 2, 3];

    // map is lazy - nothing happens until collect()
    let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
    println!("Doubled: {:?}", doubled);  // [2, 4, 6]

    // Can chain multiple maps
    let result: Vec<i32> = v.iter()
        .map(|x| x * 2)
        .map(|x| x + 1)
        .collect();
    println!("Doubled + 1: {:?}", result);  // [3, 5, 7]
}

filter - Keep Matching Elements#

The filter adapter keeps only elements that satisfy a predicate. This is equivalent to C++’s std::copy_if:

C++:

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

int main() {
  std::vector<int> v = {1, 2, 3, 4, 5};
  std::vector<int> evens;

  // copy_if is the filtering algorithm
  std::copy_if(v.begin(), v.end(), std::back_inserter(evens),
               [](int x) { return x % 2 == 0; });

  std::cout << "Evens: ";
  for (int x : evens) {
    std::cout << x << " ";  // 2 4
  }
  std::cout << "\n";

  return 0;
}

Rust:

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    // filter takes a predicate returning bool
    // Note: &&x because filter yields &T, and we're borrowing that
    let evens: Vec<i32> = v.iter()
        .filter(|&&x| x % 2 == 0)
        .cloned()  // convert &i32 to i32
        .collect();
    println!("Evens: {:?}", evens);  // [2, 4]

    // Alternative: use copied() instead of cloned() for Copy types
    let evens: Vec<i32> = v.iter()
        .filter(|&&x| x % 2 == 0)
        .copied()
        .collect();
    println!("Evens (copied): {:?}", evens);
}

filter_map - Filter and Transform#

The filter_map adapter combines filtering and mapping in one step. It takes a function returning Option<T> - Some values are kept, None values are filtered out. This is particularly useful for parsing or fallible transformations:

C++:

#include <charconv>
#include <iostream>
#include <optional>
#include <string>
#include <vector>

std::optional<int> try_parse(const std::string& s) {
  int value;
  auto result = std::from_chars(s.data(), s.data() + s.size(), value);
  if (result.ec == std::errc()) {
    return value;
  }
  return std::nullopt;
}

int main() {
  std::vector<std::string> strings = {"1", "two", "3", "four", "5"};
  std::vector<int> numbers;

  for (const auto& s : strings) {
    if (auto n = try_parse(s)) {
      numbers.push_back(*n);
    }
  }

  std::cout << "Parsed numbers: ";
  for (int x : numbers) {
    std::cout << x << " ";  // 1 3 5
  }
  std::cout << "\n";

  return 0;
}

Rust:

fn main() {
    let strings = vec!["1", "two", "3", "four", "5"];

    // filter_map: return Some to keep, None to filter out
    let numbers: Vec<i32> = strings
        .iter()
        .filter_map(|s| s.parse().ok())  // parse returns Result, .ok() converts to Option
        .collect();
    println!("Parsed numbers: {:?}", numbers);  // [1, 3, 5]

    // Equivalent using filter + map (less elegant)
    let numbers2: Vec<i32> = strings
        .iter()
        .filter(|s| s.parse::<i32>().is_ok())
        .map(|s| s.parse().unwrap())
        .collect();
    println!("Parsed (filter+map): {:?}", numbers2);
}

take and skip#

The take adapter limits iteration to the first N elements, while skip skips the first N elements. These are useful for pagination and windowing:

C++:

#include <iostream>
#include <vector>

int main() {
  std::vector<int> v = {1, 2, 3, 4, 5};

  // Take first 3 (manual loop or use ranges in C++20)
  std::cout << "First 3: ";
  for (size_t i = 0; i < 3 && i < v.size(); ++i) {
    std::cout << v[i] << " ";
  }
  std::cout << "\n";

  // Skip first 2
  std::cout << "Skip 2: ";
  for (size_t i = 2; i < v.size(); ++i) {
    std::cout << v[i] << " ";
  }
  std::cout << "\n";

  return 0;
}

Rust:

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    // take: limit to first N elements
    let first_three: Vec<_> = v.iter().take(3).collect();
    println!("First 3: {:?}", first_three);  // [1, 2, 3]

    // skip: skip first N elements
    let skip_two: Vec<_> = v.iter().skip(2).collect();
    println!("Skip 2: {:?}", skip_two);  // [3, 4, 5]

    // Combine for pagination
    let page_size = 2;
    let page = 1;  // 0-indexed
    let page_items: Vec<_> = v.iter()
        .skip(page * page_size)
        .take(page_size)
        .collect();
    println!("Page 1 (size 2): {:?}", page_items);  // [3, 4]

    // take_while and skip_while use predicates
    let until_four: Vec<_> = v.iter().take_while(|&&x| x < 4).collect();
    println!("Take while < 4: {:?}", until_four);  // [1, 2, 3]
}

enumerate - Add Indices#

The enumerate adapter pairs each element with its index. This is cleaner than maintaining a separate counter variable:

C++:

#include <iostream>
#include <vector>
#include <string>

int main() {
  std::vector<std::string> v = {"apple", "banana", "cherry"};

  // Manual index tracking
  size_t i = 0;
  for (const auto& x : v) {
    std::cout << i << ": " << x << "\n";
    ++i;
  }

  // Or use traditional for loop
  for (size_t i = 0; i < v.size(); ++i) {
    std::cout << i << ": " << v[i] << "\n";
  }

  return 0;
}

Rust:

fn main() {
    let v = vec!["apple", "banana", "cherry"];

    // enumerate yields (index, element) tuples
    for (i, x) in v.iter().enumerate() {
        println!("{}: {}", i, x);
    }

    // Can destructure in closures too
    let indexed: Vec<_> = v.iter()
        .enumerate()
        .map(|(i, s)| format!("{}: {}", i, s))
        .collect();
    println!("Indexed: {:?}", indexed);
}

zip - Combine Iterators#

The zip adapter combines two iterators into one that yields pairs. Iteration stops when either iterator is exhausted:

C++:

#include <iostream>
#include <vector>
#include <string>

int main() {
  std::vector<int> numbers = {1, 2, 3};
  std::vector<std::string> words = {"one", "two", "three"};

  // Manual parallel iteration
  for (size_t i = 0; i < numbers.size() && i < words.size(); ++i) {
    std::cout << numbers[i] << " -> " << words[i] << "\n";
  }

  return 0;
}

Rust:

fn main() {
    let numbers = vec![1, 2, 3];
    let words = vec!["one", "two", "three"];

    // zip combines two iterators into pairs
    for (n, w) in numbers.iter().zip(words.iter()) {
        println!("{} -> {}", n, w);
    }

    // Collect into vector of tuples
    let pairs: Vec<_> = numbers.iter().zip(words.iter()).collect();
    println!("Pairs: {:?}", pairs);

    // Unequal lengths: stops at shorter
    let short = vec![1, 2];
    let long = vec!["a", "b", "c", "d"];
    let zipped: Vec<_> = short.iter().zip(long.iter()).collect();
    println!("Unequal zip: {:?}", zipped);  // [(1, "a"), (2, "b")]
}

chain - Concatenate Iterators#

The chain adapter concatenates two iterators, yielding all elements from the first followed by all elements from the second:

C++:

#include <iostream>
#include <vector>

int main() {
  std::vector<int> a = {1, 2};
  std::vector<int> b = {3, 4};

  // Manual concatenation
  std::vector<int> combined;
  combined.insert(combined.end(), a.begin(), a.end());
  combined.insert(combined.end(), b.begin(), b.end());

  std::cout << "Combined: ";
  for (int x : combined) {
    std::cout << x << " ";
  }
  std::cout << "\n";

  return 0;
}

Rust:

fn main() {
    let a = vec![1, 2];
    let b = vec![3, 4];

    // chain concatenates iterators lazily
    let combined: Vec<_> = a.iter().chain(b.iter()).collect();
    println!("Combined: {:?}", combined);  // [1, 2, 3, 4]

    // Can chain multiple iterators
    let c = vec![5, 6];
    let all: Vec<_> = a.iter()
        .chain(b.iter())
        .chain(c.iter())
        .collect();
    println!("All: {:?}", all);  // [1, 2, 3, 4, 5, 6]
}

flatten - Flatten Nested Iterators#

The flatten adapter flattens nested iterators (or collections) into a single iterator. This is useful for working with nested data structures:

C++:

#include <iostream>
#include <vector>

int main() {
  std::vector<std::vector<int>> nested = {{1, 2}, {3, 4}, {5}};

  // Manual flattening
  std::vector<int> flat;
  for (const auto& inner : nested) {
    for (int x : inner) {
      flat.push_back(x);
    }
  }

  std::cout << "Flattened: ";
  for (int x : flat) {
    std::cout << x << " ";
  }
  std::cout << "\n";

  return 0;
}

Rust:

fn main() {
    let nested = vec![vec![1, 2], vec![3, 4], vec![5]];

    // flatten collapses one level of nesting
    let flat: Vec<_> = nested.iter().flatten().collect();
    println!("Flattened: {:?}", flat);  // [1, 2, 3, 4, 5]

    // flat_map combines map + flatten (very common pattern)
    let words = vec!["hello", "world"];
    let chars: Vec<_> = words.iter()
        .flat_map(|s| s.chars())
        .collect();
    println!("All chars: {:?}", chars);  // ['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']
}

Consuming Adapters#

Consuming adapters (also called “terminal operations”) consume the iterator and produce a final result. Unlike adapters like map and filter, these methods trigger actual iteration.

collect - Gather into Collection#

The collect method consumes an iterator and gathers elements into a collection. The target type is inferred from context or specified with turbofish syntax:

C++:

#include <iostream>
#include <set>
#include <string>
#include <vector>

int main() {
  // Collecting into vector (manual)
  std::vector<int> v;
  for (int i = 1; i <= 5; ++i) {
    v.push_back(i);
  }

  // Collecting into set
  std::set<int> s(v.begin(), v.end());

  // Collecting chars into string
  std::vector<char> chars = {'h', 'e', 'l', 'l', 'o'};
  std::string str(chars.begin(), chars.end());

  std::cout << "String: " << str << "\n";

  return 0;
}

Rust:

use std::collections::HashSet;

fn main() {
    // Collect range into Vec
    let v: Vec<i32> = (1..=5).collect();
    println!("Vec: {:?}", v);

    // Collect into HashSet (removes duplicates)
    let set: HashSet<i32> = vec![1, 2, 2, 3, 3, 3].into_iter().collect();
    println!("Set: {:?}", set);

    // Collect chars into String
    let s: String = ['h', 'e', 'l', 'l', 'o'].iter().collect();
    println!("String: {}", s);

    // Turbofish syntax when type can't be inferred
    let v = (1..=5).collect::<Vec<_>>();
    println!("Turbofish: {:?}", v);
}

fold and reduce - Accumulate Values#

The fold method accumulates values with an initial accumulator, while reduce uses the first element as the initial value. These are equivalent to C++’s std::accumulate and std::reduce:

C++:

#include <iostream>
#include <numeric>
#include <vector>

int main() {
  std::vector<int> v = {1, 2, 3, 4, 5};

  // accumulate with initial value (like fold)
  int sum = std::accumulate(v.begin(), v.end(), 0,
                            [](int acc, int x) { return acc + x; });
  std::cout << "Sum: " << sum << "\n";  // 15

  // reduce without initial value (C++17)
  // Note: std::reduce may reorder operations for parallelism
  int product = std::reduce(v.begin(), v.end(), 1, std::multiplies<int>());
  std::cout << "Product: " << product << "\n";  // 120

  return 0;
}

Rust:

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    // fold: requires initial value, always succeeds
    let sum = v.iter().fold(0, |acc, x| acc + x);
    println!("Sum (fold): {}", sum);  // 15

    // reduce: uses first element as initial, returns Option
    let product = v.iter().copied().reduce(|acc, x| acc * x);
    println!("Product (reduce): {:?}", product);  // Some(120)

    // reduce on empty iterator returns None
    let empty: Vec<i32> = vec![];
    let result = empty.iter().copied().reduce(|acc, x| acc + x);
    println!("Empty reduce: {:?}", result);  // None

    // Building a string with fold
    let words = vec!["hello", "world"];
    let sentence = words.iter().fold(String::new(), |mut acc, &word| {
        if !acc.is_empty() {
            acc.push(' ');
        }
        acc.push_str(word);
        acc
    });
    println!("Sentence: {}", sentence);  // "hello world"
}

sum and product#

The sum and product methods are specialized folds for numeric types:

C++:

#include <iostream>
#include <numeric>
#include <vector>

int main() {
  std::vector<int> v = {1, 2, 3, 4, 5};

  int sum = std::accumulate(v.begin(), v.end(), 0);
  int product = std::accumulate(v.begin(), v.end(), 1, std::multiplies<int>());

  std::cout << "Sum: " << sum << "\n";      // 15
  std::cout << "Product: " << product << "\n";  // 120

  return 0;
}

Rust:

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    // sum and product require type annotation
    let sum: i32 = v.iter().sum();
    let product: i32 = v.iter().product();

    println!("Sum: {}", sum);      // 15
    println!("Product: {}", product);  // 120

    // Works with floating point too
    let floats = vec![1.5, 2.5, 3.0];
    let sum: f64 = floats.iter().sum();
    println!("Float sum: {}", sum);  // 7.0
}

find and position#

The find method returns the first element matching a predicate, while position returns its index:

C++:

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

int main() {
  std::vector<int> v = {1, 2, 3, 4, 5};

  // find returns iterator
  auto it = std::find_if(v.begin(), v.end(), [](int x) { return x > 3; });
  if (it != v.end()) {
    std::cout << "Found: " << *it << "\n";  // 4
  }

  // position requires distance calculation
  auto pos = std::distance(v.begin(), it);
  std::cout << "Position: " << pos << "\n";  // 3

  return 0;
}

Rust:

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    // find returns Option<&T>
    let found = v.iter().find(|&&x| x > 3);
    println!("Found: {:?}", found);  // Some(&4)

    // position returns Option<usize>
    let pos = v.iter().position(|&x| x > 3);
    println!("Position: {:?}", pos);  // Some(3)

    // Not found returns None
    let not_found = v.iter().find(|&&x| x > 10);
    println!("Not found: {:?}", not_found);  // None

    // find_map combines find and map
    let strings = vec!["1", "two", "3"];
    let first_num: Option<i32> = strings.iter().find_map(|s| s.parse().ok());
    println!("First parseable: {:?}", first_num);  // Some(1)
}

any and all#

The any method returns true if any element matches, while all returns true if all elements match. These short-circuit on the first decisive result:

C++:

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

int main() {
  std::vector<int> v = {1, 2, 3, 4, 5};

  bool has_even = std::any_of(v.begin(), v.end(), [](int x) { return x % 2 == 0; });
  bool all_positive = std::all_of(v.begin(), v.end(), [](int x) { return x > 0; });
  bool none_negative = std::none_of(v.begin(), v.end(), [](int x) { return x < 0; });

  std::cout << "Has even: " << (has_even ? "yes" : "no") << "\n";
  std::cout << "All positive: " << (all_positive ? "yes" : "no") << "\n";
  std::cout << "None negative: " << (none_negative ? "yes" : "no") << "\n";

  return 0;
}

Rust:

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    // any: true if any element matches
    let has_even = v.iter().any(|&x| x % 2 == 0);
    println!("Has even: {}", has_even);  // true

    // all: true if all elements match
    let all_positive = v.iter().all(|&x| x > 0);
    println!("All positive: {}", all_positive);  // true

    // Short-circuit behavior
    let v2 = vec![1, 2, 3, 4, 5];
    let found_early = v2.iter().any(|&x| {
        println!("Checking {}", x);
        x == 2
    });
    // Only prints "Checking 1" and "Checking 2"
    println!("Found: {}", found_early);
}

count, min, and max#

These methods provide basic statistics about iterator elements:

C++:

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

int main() {
  std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};

  size_t count = v.size();
  auto [min_it, max_it] = std::minmax_element(v.begin(), v.end());

  std::cout << "Count: " << count << "\n";
  std::cout << "Min: " << *min_it << "\n";
  std::cout << "Max: " << *max_it << "\n";

  return 0;
}

Rust:

fn main() {
    let v = vec![3, 1, 4, 1, 5, 9, 2, 6];

    // count consumes the iterator
    let count = v.iter().count();
    println!("Count: {}", count);  // 8

    // min and max return Option (None for empty iterators)
    let min = v.iter().min();
    let max = v.iter().max();
    println!("Min: {:?}", min);  // Some(&1)
    println!("Max: {:?}", max);  // Some(&9)

    // min_by and max_by for custom comparison
    let words = vec!["apple", "pie", "extraordinary"];
    let longest = words.iter().max_by_key(|s| s.len());
    println!("Longest: {:?}", longest);  // Some(&"extraordinary")
}

C++ Comparison#

This section provides a comprehensive comparison between C++ STL algorithms and Rust iterator methods. C++ traditionally uses algorithm functions that take iterator pairs, while Rust uses method chaining on iterator objects.

C++ STL Algorithms vs Rust Iterators#

The following example shows a complete transformation pipeline in both languages. Notice how Rust’s method chaining is more concise and the lazy evaluation allows the compiler to optimize the entire chain:

C++ with algorithms:

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

int main() {
  std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

  // Transform: double each element
  std::vector<int> doubled;
  std::transform(v.begin(), v.end(), std::back_inserter(doubled),
                 [](int x) { return x * 2; });

  // Filter: keep only evens (from original)
  std::vector<int> evens;
  std::copy_if(v.begin(), v.end(), std::back_inserter(evens),
               [](int x) { return x % 2 == 0; });

  // Sum
  int sum = std::accumulate(v.begin(), v.end(), 0);

  // Chained operations require intermediate vectors
  std::vector<int> temp;
  std::copy_if(v.begin(), v.end(), std::back_inserter(temp),
               [](int x) { return x % 2 == 0; });
  std::vector<int> result;
  std::transform(temp.begin(), temp.end(), std::back_inserter(result),
                 [](int x) { return x * 2; });

  std::cout << "Doubled: ";
  for (int x : doubled) std::cout << x << " ";
  std::cout << "\nEvens: ";
  for (int x : evens) std::cout << x << " ";
  std::cout << "\nSum: " << sum;
  std::cout << "\nFiltered+Doubled: ";
  for (int x : result) std::cout << x << " ";
  std::cout << "\n";

  return 0;
}

Rust equivalent:

fn main() {
    let v = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // Transform: double each element
    let doubled: Vec<_> = v.iter().map(|x| x * 2).collect();

    // Filter: keep only evens
    let evens: Vec<_> = v.iter().filter(|&&x| x % 2 == 0).collect();

    // Sum
    let sum: i32 = v.iter().sum();

    // Chained operations - no intermediate allocations!
    let result: Vec<_> = v.iter()
        .filter(|&&x| x % 2 == 0)
        .map(|x| x * 2)
        .collect();

    println!("Doubled: {:?}", doubled);
    println!("Evens: {:?}", evens);
    println!("Sum: {}", sum);
    println!("Filtered+Doubled: {:?}", result);
}

C++20 Ranges#

C++20 introduced ranges, which provide a more Rust-like experience with lazy evaluation and method chaining via the pipe operator:

C++20 Ranges:

#include <iostream>
#include <ranges>
#include <vector>

int main() {
  std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

  // Pipe syntax similar to Rust's method chaining
  auto result = v
      | std::views::filter([](int x) { return x % 2 == 0; })
      | std::views::transform([](int x) { return x * 2; });

  // Lazy evaluation - nothing computed until iteration
  std::cout << "Filtered and doubled evens: ";
  for (int x : result) {
    std::cout << x << " ";  // 4 8 12 16 20
  }
  std::cout << "\n";

  // Take first N elements
  auto first_three = v | std::views::take(3);
  std::cout << "First three: ";
  for (int x : first_three) {
    std::cout << x << " ";
  }
  std::cout << "\n";

  return 0;
}

Rust equivalent:

fn main() {
    let v = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // Method chaining (Rust's native style)
    let result: Vec<_> = v.iter()
        .filter(|&&x| x % 2 == 0)
        .map(|x| x * 2)
        .collect();

    println!("Filtered and doubled evens: {:?}", result);  // [4, 8, 12, 16, 20]

    // Take first N elements
    let first_three: Vec<_> = v.iter().take(3).collect();
    println!("First three: {:?}", first_three);
}

Creating Custom Iterators#

You can create custom iterators by implementing the Iterator trait. This requires defining the Item associated type and the next method. Once implemented, your type automatically gains access to all iterator adapter methods.

The following example creates a counter iterator that yields numbers from 1 to a maximum value:

C++:

#include <iostream>
#include <iterator>

class Counter {
public:
  using iterator_category = std::input_iterator_tag;
  using value_type = int;
  using difference_type = std::ptrdiff_t;
  using pointer = int*;
  using reference = int&;

private:
  int count_;
  int max_;

public:
  Counter(int max) : count_(0), max_(max) {}

  // For end sentinel
  static Counter end() {
    Counter c(0);
    c.count_ = c.max_ + 1;
    return c;
  }

  int operator*() const { return count_; }

  Counter& operator++() {
    ++count_;
    return *this;
  }

  bool operator!=(const Counter& other) const {
    return count_ <= max_;
  }
};

int main() {
  // Manual iteration
  int sum = 0;
  for (Counter c(5); c != Counter::end(); ++c) {
    if (*c > 0) {  // skip 0
      sum += *c;
    }
  }
  std::cout << "Sum 1-5: " << sum << "\n";  // 15

  return 0;
}

Rust:

struct Counter {
    count: u32,
    max: u32,
}

impl Counter {
    fn new(max: u32) -> Self {
        Counter { count: 0, max }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.count < self.max {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

fn main() {
    // All iterator methods are automatically available
    let sum: u32 = Counter::new(5).sum();
    println!("Sum 1-5: {}", sum);  // 15

    // Can use any adapter
    let evens: Vec<_> = Counter::new(10)
        .filter(|&x| x % 2 == 0)
        .collect();
    println!("Evens 1-10: {:?}", evens);  // [2, 4, 6, 8, 10]

    // Chaining with other iterators
    let doubled: Vec<_> = Counter::new(3)
        .map(|x| x * 2)
        .collect();
    println!("Doubled 1-3: {:?}", doubled);  // [2, 4, 6]
}

See Also#

  • Collections - Collections that implement Iterator

  • Closures - Closures used with iterator adapters