How to loop over 0 .. v.len() - 1 when v might be empty?

181 views Asked by At

What's the correct way to loop over 0 .. v.len() - 1 when the vector v might be empty?

Consider the following code, which prints all choices of 2 elements in a vector [5, 6, 7]:

fn main() {
  let v: Vec<i32> = vec![5, 6, 7];
  for i in 0 .. v.len() - 1 {
    for j in i + 1 .. v.len() {
      println!("{} {}", v[i], v[j]);
    }
  }
}

It prints

5 6
5 7
6 7

as expected.

Now, if we instead have

let v: Vec<i32> = vec![];

then the program panics due to overflow, which is also expected.

Q: How to fix it without changing the structure and the logic of the program, and without involving signed integers?

The following are some examples of the workarounds that I don't want:

  • Handling empty v separately (changes the logic). Overall, I don't want any checks that v is empty, e.g. by if v.len() == 0 { 0 } else { v.len() - 1 }

  • Changing the order of loops (changes the logic and the output):

    for j in 0 .. v.len() {
        for i in 0 .. j {
    
  • Shifting the indices (changes the logic):

    for i in 1 .. v.len() {
        for j in i .. v.len() {
            // Now i is shifted by 1
    
  • Using i32 (casts):

    for i in 0 .. v.len() as i32 - 1 {
        for j in i + 1 .. v.len() as i32 {
            // cast i and j to usize
    

Overall, I hope that there is a clean way to handle 0 .. v.len() - 1 without any hacks.

5

There are 5 answers

1
jthulhu On BEST ANSWER

Notice that, if i == v.len()-1, nothing happens in the body of the outer loop, because i+1..v.len() is an empty iterator. Therefore, the simplest way to avoid integer underflows is simply to remove the -1:

fn main() {
  let v: Vec<i32> = vec![5, 6, 7];
  for i in 0 .. v.len() {
    for j in i + 1 .. v.len() {
      println!("{} {}", v[i], v[j]);
    }
  }
}

produces

5 6
5 7
6 7

and if I replace v with Vec::new(), you get no output, as expected.

0
devio On

To address the issue of handling 0 .. v.len() - 1 when a vector v might be empty, we can use Rust's checked_sub method to safely subtract one from the length of the vector without risking an integer underflow. This method returns an Option that is None if the subtraction would result in an underflow, and Some with the result otherwise.

And here's how you can implement it in the code:

fn main() {
    let v: Vec<i32> = vec![5, 6, 7];
    if let Some(max_index) = v.len().checked_sub(1) {
        for i in 0..max_index {
            for j in i + 1..v.len() {
                println!("{} {}", v[i], v[j]);
            }
        }
    }
    // If v is empty, the loop will not execute, avoiding the panic.
}

This code will work correctly even if the vector v is empty, as checked_sub(1) will return None and the loop will not execute. This solution does not change the structure or logic of the program, does not involve signed integers, and does not require handling the empty vector case separately.

0
Dmitry On

After a bit more googling, I found saturating_sub which "saturates at the numeric bounds instead of overflowing." So, 0usize.saturating_sub(1usize) will be 0usize, which results in range 0usize .. 0usize, as desired.

fn main() {
  let v: Vec<i32> = vec![];
  for i in 0 .. v.len().saturating_sub(1) {
    for j in i + 1 .. v.len() {
      println!("{} {}", v[i], v[j]);
    }
  }
}
2
Kaplan On

What's wrong with doing it properly and checking the vector-is-empty case explicitly?

let v: Vec<i32> = vec![5, 6, 7];
if !v.is_empty() {
    for i in 0..v.len() - 1 {
        for j in i + 1..v.len() {
            println!("{} {}", v[i], v[j]);
        }
    }
}

BTW: It also makes no sense to run the code in a special case with a vector of length 1, so checking for v.len() >= 2 would be even more correct.

0
Francis Gagné On

Iterators give us the tools we need here, no need to use indices.

fn main() {
    let v: Vec<i32> = vec![5, 6, 7];
    let mut v_iter = v.iter();
    while let Some(x) = v_iter.next() {
        for y in v_iter.as_slice() {
            println!("{x} {y}");
        }
    }
}

NOTE: Just like in jthulhu's answer, this relies on the fact that the inner loop does nothing on the outer loop's last iteration.

std::slice::Iter, the iterator type for slices (as returned by slice.iter()), has a method named as_slice that returns a slice of the remaining elements in the iterator. In order to be able to access this, we need to handle the iterator explicitly in the outer loop instead of having the for loop do this for us. This while let Some(x) = v_iter.next() pattern is basically what a for loop desugars to, with the major exception that the while let form doesn't consume the iterator, so we can use it within or even after the loop.

We can achieve the same result using clone instead of as_slice in this case, because we only use the result as an iterator. While as_slice is specific to the slice iterator (and perhaps a few others), clone is available on many more iterator types.

fn main() {
    let v: Vec<i32> = vec![5, 6, 7];
    let mut v_iter = v.iter();
    while let Some(x) = v_iter.next() {
        for y in v_iter.clone() {
            println!("{x} {y}");
        }
    }
}