Sling Academy
Home/Rust/Rust - Verifying iterator invalidation rules: modifying vectors during iteration

Rust - Verifying iterator invalidation rules: modifying vectors during iteration

Last updated: January 07, 2025

Rust is a powerful systems programming language that prides itself on safety and performance. One of its distinctive features is the emphasis on memory safety without a garbage collector. This post focuses on a crucial aspect of Rust that often traps beginners: iterator invalidation when modifying vectors during iteration.

Understanding Iterators in Rust

An iterator in Rust is any object that implements the Iterator trait. The most common use of iterators is to loop over data structures like vectors, arrays, or other collections. Below is a simple example of how an iterator is used on a vector:

fn main() {
    let v = vec![1, 2, 3, 4, 5];
    for val in v.iter() {
        println!("{}", val);
    }
}

This will print each value in the vector. Rust's safety guarantees mean if you modify the vector while iterating, you might run into problems. Let’s delve deeper into these iterator invalidation rules.

Why Iterators Can Become Invalid

The core reason why iterators can become invalid is due to changes in the structure’s state they’re iterating over. Specifically, modifying a vector has implications for its underlying buffer and size, potentially invalidating the iterator. Here is why:

1. **Allocations:** Rust's Vec might reallocate memory when resized, invalidating raw pointers. 2. **Out-of-bounds removal:** Removing elements can lead to iterator state becoming invalid because of changes to the vector memory.

Modifying Vectors During Iteration – Undefined Behavior!

Trying to modify a vector while iterating over it using its iterator can lead to undefined behavior, a dangerous land in systems programming, notoriously avoided by Rust. Let's look at an example:

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];
 
    for val in v.iter_mut() {
        *val += 1;  // This is allowed
    }
    
    println!("{:?}", v);
    // Prints: [2, 3, 4, 5, 6]
}

In the above code, modifying the element directly through iter_mut() is safe, because you process each value directly by borrowing them mutably. But what if you tried to push more elements to the vector within this loop?

Example of Unsafe Behavior

fn unsafe_modification() {
    let mut v = vec![1, 2, 3, 4, 5];
    
    for val in v.iter() {
        v.push(*val + 10);  // Unsafe!
        println!("{:?}", v);
    }
}

fn main() {
    unsafe_modification();
    // Runtime error due to iterator invalidation
}

In this example, by modifying the vector v within the loop, you're altering the state of the container that the iterator references.

Safe Approach: Using Indices

A safe alternative to avoid these problems is iterating over indices when you plan to modify the vector.

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];
    
    let size = v.len();
    for i in 0..size {
        v.push(v[i] + 10);  // Safe!
    }
    
    println!("{:?}", v);
    // Prints: [1, 2, 3, 4, 5, 11, 12, 13, 14, 15]
}

In the code snippet above, by using an index-based loop and obtaining the initial size upfront, the changes to the vector's length don't interfere with the control structure of the loop.

Conclusion

Using iterators in Rust is both powerful and complex, especially when modifying the underlying collection during iteration. Understanding how iterator invalidation works and designing safe patterns, such as indexing, ensures your Rust programs remain bug-free and adhere to memory safety rules. Always test any modifications by examining how they affect memory and ensure you use iterators safely.

Next Article: Rust - Optimizing random access in large vectors with chunked approaches

Previous Article: Rust - Handling generics and trait bounds for flexible vector or map manipulations

Series: Collections in Rust

Rust

You May Also Like

  • E0557 in Rust: Feature Has Been Removed or Is Unavailable in the Stable Channel
  • Network Protocol Handling Concurrency in Rust with async/await
  • Using the anyhow and thiserror Crates for Better Rust Error Tests
  • Rust - Investigating partial moves when pattern matching on vector or HashMap elements
  • Rust - Handling nested or hierarchical HashMaps for complex data relationships
  • Rust - Combining multiple HashMaps by merging keys and values
  • Composing Functionality in Rust Through Multiple Trait Bounds
  • E0437 in Rust: Unexpected `#` in macro invocation or attribute
  • Integrating I/O and Networking in Rust’s Async Concurrency
  • E0178 in Rust: Conflicting implementations of the same trait for a type
  • Utilizing a Reactor Pattern in Rust for Event-Driven Architectures
  • Parallelizing CPU-Intensive Work with Rust’s rayon Crate
  • Managing WebSocket Connections in Rust for Real-Time Apps
  • Downloading Files in Rust via HTTP for CLI Tools
  • Mocking Network Calls in Rust Tests with the surf or reqwest Crates
  • Rust - Designing advanced concurrency abstractions using generic channels or locks
  • Managing code expansion in debug builds with heavy usage of generics in Rust
  • Implementing parse-from-string logic for generic numeric types in Rust
  • Rust.- Refining trait bounds at implementation time for more specialized behavior