Armadillan
Armadillan

Reputation: 570

Why can I start a slice past the end of a vector in Rust?

Given v = vec![1,2,3,4], why does v[4..] return an empty vector, but v[5..] panics, while both v[4] and v[5] panic? I suspect this has to do with the implementation of slicing without specifying either the start- or endpoint, but I couldn't find any information on this online.

Upvotes: 16

Views: 5218

Answers (2)

Daniel
Daniel

Reputation: 1507

While the other answer explains how to understand and remember the indexing behavior implemented in Rust standard library, the real reason why it is the way it is has nothing to do with technical limitations. It comes down to the design decision made by the authors of Rust standard library.

Given v = vec![1,2,3,4], why does v[4..] return an empty vector, but v[5..] panics [..] ?

Because it was decided so. The code below that handles slice indexing (full source) will panic if the start index is larger than the slice's length.

fn index(self, slice: &[T]) -> &[T] {
    if self.start > slice.len() {
        slice_start_index_len_fail(self.start, slice.len());
    }
    // SAFETY: `self` is checked to be valid and in bounds above.
    unsafe { &*self.get_unchecked(slice) }
}

fn slice_start_index_len_fail(index: usize, len: usize) -> ! {
    panic!("range start index {} out of range for slice of length {}", index, len);
}

How could it be implemented differently? I personally like how Python does it.

v = [1, 2, 3, 4]

a = v[4]   # -> Raises an exception        - Similar to Rust's behavior (panic)
b = v[5]   # -> Same, raises an exception  - Also similar to Rust's

# (Equivalent to Rust's v[4..])
w = v[4:]  # -> Returns an empty list      - Similar to Rust's
x = v[5:]  # -> Also returns an empty list - Different from Rust's, which panics

Python's approach is not necessarily better than Rust's, because there's always a trade-off. Python's approach is more convenient (there's no need to check if a start index is not greater than the length), but if there's a bug, it's harder to find because it doesn't fail early.

Although Rust can technically follow Python's approach, its designers decided to fail early by panicking in order that a bug can be faster to find, but with a cost of some inconvenience (programmers need to ensure that a start index is not greater than the length).

Upvotes: 4

user2722968
user2722968

Reputation: 16475

This is simply because std::ops::RangeFrom is defined to be "bounded inclusively below".

A quick recap of all the plumbing: v[4..] desugars to std::ops::Index using 4.. (which parses as a std::ops::RangeFrom) as the parameter. std::ops::RangeFrom implements std::slice::SliceIndex and Vec has an implementation for std::ops::Index for any parameter that implements std::slice::SliceIndex. So what you are looking at is a RangeFrom being used to std::ops::Index the Vec.

std::ops::RangeFrom is defined to always be inclusive on the lower bound. For example [0..] will include the first element of the thing being indexed. If (in your case) the Vec is empty, then [0..] will be the empty slice. Notice: if the lower bound wasn't inclusive, there would be no way to slice an empty Vec at all without causing a panic, which would be cumbersome.

A simple way to think about it is "where the fence-post is put".

A v[0..] in a vec![0, 1, 2 ,3] is

|  0    1    2    3   |
  ^
  |- You are slicing from here. This includes the
     entire `Vec` (even if it was empty)

In v[4..] it is

|  0    1    2    3   |
                    ^
                    |- You are slicing from here to the end of the Vector.
                       Which results in, well, nothing.

while a v[5..] would be

|  0    1    2    3   |
                        ^
                        |- Slicing from here to infinity is definitely
                           outside the `Vec` and, also, the
                           caller's fault, so panic!

and a v[3..] is

|  0    1    2    3   |
                ^
                |- slicing from here to the end results in `&[3]`

Upvotes: 17

Related Questions