Iterators and algorithms: C++ and Rust

A C++ container gives the programmer a pair of iterators, and a loop advances one until it reaches the other. Rust gives an Iterator value that produces the items itself, and a for loop calls it. The difference is who owns the position and what happens when the container changes during the walk.

The task in C++

The vector is summed with a range-for and printed with an explicit iterator pair. The two loops read the same three values, and the total is sixty.

#include <iostream> #include <vector> int main() { std::vector<int> values{10, 20, 30}; int total = 0; for (int value : values) { total += value; } for (auto it = values.begin(); it != values.end(); ++it) { std::cout << *it << ' '; } std::cout << '\n' << total << '\n'; }

g++ -std=c++23 -Wall -Wextra iterators.cpp -o iterators
./iterators

10 20 30
60

The iterator is a pointer-like object, and end() is one past the last element. The loop is valid as long as the vector does not reallocate.

The same task in Rust

values.iter() produces the references, sum consumes them, and a second iterator prints the values. The vector is borrowed for the length of each chain.

fn main() { let values = vec![10, 20, 30]; let total: i32 = values.iter().sum(); for value in &values { print!("{value} "); } println!("\n{total}"); }

rustc --edition 2024 iterators.rs
./iterators

10 20 30
60

The output is identical. The lesson on iterators writes the trait by hand and shows what the chain calls underneath.

What differs and why

Who owns the position: in C++ the iterator is a separate object the loop advances, and the container does not know about it. In Rust the iterator borrows the container, and the borrow checker ties its lifetime to the collection.

What the compiler rejects: C++ accepts a push_back during the walk, and the iterators become invalid; the behaviour is undefined. Rust rejects the same program with E0502, because the loop holds a shared borrow and the push needs a mutable one.

What happens at run time: the C++ program may print the old three values and a size of four, or crash, or print garbage; it depends on whether the push reallocated. The Rust program does not exist.

The habit that breaks

The habit is to modify the container inside the loop. The C++ version below compiles with no warning, and the fix there is to collect the additions and apply them after the loop.

#include <iostream> #include <vector> int main() { std::vector<int> values{1, 2, 3}; for (int value : values) { if (value == 2) { values.push_back(4); } std::cout << value << ' '; } std::cout << '\n' << values.size() << '\n'; }

g++ -std=c++23 -Wall -Wextra invalidate.cpp -o invalidate
echo "status: $?"

status: 0

The Rust version is rejected at the push, and the message names the borrow that the loop holds. Collecting into a temporary vector first is the fix in both languages.

fn main() { let mut values = vec![1, 2, 3]; for value in &values { if *value == 2 { values.push(4); } println!("{value}"); } }

rustc --edition 2024 invalidate.rs

error[E0502]: cannot borrow `values` as mutable because it is also borrowed as immutable
--> invalidate.rs:5:13
|
3 | for value in &values {
| -------
| |
| immutable borrow occurs here
| immutable borrow later used here
4 | if *value == 2 {
5 | values.push(4);
| ^^^^^^^^^^^^^^ mutable borrow occurs here

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0502`.

The C++ lesson for this ground is Iterators, and the Rust lesson is The Iterator trait. The next page in this set is Testing.

The C++ programs were built with g++ 15.2.0 and the Rust programs with rustc 1.96.1, and both were run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev