Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

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.

like image 778
Armadillan Avatar asked Feb 06 '21 02:02

Armadillan


People also ask

How do you find the last element of a vector in Rust?

Assign to variable x the last element of list items. let x = items. last(). unwrap();

How do you add slices in Rust?

As a result, you cannot use a Rust slice to insert, append or remove elements from the underlying container. Instead, you need either: to use a mutable reference to the container itself, to design a trait and use a mutable reference to said trait.

How do you initialize a vector in Rust?

In Rust, there are several ways to initialize a vector. In order to initialize a vector via the new() method call, we use the double colon operator: let mut vec = Vec::new();

What is a VEC in Rust?

Vector is a module in Rust that provides the container space to store values. It is a contiguous resizable array type, with heap-allocated contents. It is denoted by Vec<T>. Vectors in Rust have O(1) indexing and push and pop operations in vector also take O(1) complexity.


2 Answers

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]`
like image 88
user2722968 Avatar answered Dec 13 '22 10:12

user2722968


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).

like image 21
Daniel Avatar answered Dec 13 '22 10:12

Daniel