Confused about slice methods

Considering a vector:

let v = vec![1, 2, 3, 4, 5];

I understand doing this is not allowed:

let a = v[..];

But this syntax seems to work, the following code compiles just fine:

let b = v[..].starts_with(&[1,2]);

If creating v[..] is not allowed why I am allowed to do it inline and call methods on it? Is v[..] a different type than slice?

this is a language feature called "automatic referencing and dereferencing": in short, the starts_with() method takes a shared reference &self as receiver, when you use the method call syntax, the compiler automatically create a reference.

note: you cannot create a value of slice because a slice is a "dynamically sized type", or DST for short, but a slice reference is fine:

let v = vec![1, 2, 3, 4, 5];
let a = &v[..];
2 Likes

The core concept to learn here is one called a "place expression", as opposed to a "value expression". In other systems languages like C/C++, these have names like "l*value" vs "rvalue". [The rationale of this naming is that in an assignment expr1 = expr2, the left-hand side is an "l value" and the right-hand side is an "r value".] Place expressions in Rust don't refer to a value that is first created by evaluating the expression, instead they refer to a value that (generally) already existed somewhere previously.

As such, place expressions can come with ownership restrictions. You can move out of a place, but not all places support moving. You can borrow a place, but that can also be restricted, e.g. some places might only allow borrowing immutably, etc…


The easiest example of a place expression is the name of a variable in scope.

If you have let x = 1; and then write let y = x; then you’re reading (moving; or in this case copying) the value from the place x; but the place x also supports borrowing; e.g. if you write &mut x, that borrows more than just "the value 1", it borrows the value 1 in its original place, and mutations affect the original variable x.

A common example of a places that don't support moving is places (with non-Copy type) behind a reference.

let mut x = Vec::<u8>::new();

let r = &mut x;

// consider the expression `*r` of type `Vec<u8>`

// let v = *r; // doesn't work
// --> error[E0507]: cannot move out of `*r` which is behind a mutable reference

// but taking a reference of *that* place, e.g. `& *r`, of type `&Vec<u8>` is okay

let r2: &Vec<u8> = & *r;

maybe you're not too familiar with cases like &*r yet, but dereferencing is often implicit, e.g. with a field access, so a more common example would be something like

struct MyStruct {
    field: Vec<u8>,
}

impl MyStruct {
    fn add_value(&mut self, val: u8) {
        // in `self`, we have `self.field` of type `Vec<u8>`

        // we can't move its value:

        // let field_value = self.field; // doesn't work
        // --> error[E0507]: cannot move out of `self.field` which is behind a mutable reference
        
        // but we can call the `push` method
        self.field.push(val);
    }
}

this does however desugar to something similar, in this case self.field.push(val) desugars to something like

Vec::<u8>::push(&mut (*self).field, val);

where self is dereferenced, then the field is accessed, a reference to that field is created, and finally push is called. This also shows some further operations place expressions do support, that is: you can access a field of a place (which then also is a place, which inherits many restrictions such as whether or not moves, or certain types of borrows are allowed).


And indeed method calls are also possible on places, and often times don't involve any moving of the value, but only borrowing. This relates to automatic referencing, as @nerditation above explained, though automatic referencing alone doesn't really answer your question, since (partially) desugaring

v[..].starts_with(&[1,2])

into something like

(&v[..]).starts_with(&[1,2])

would't explain why a = v[..] is not allowed but &v[..] is allowed.


Finally, this example also involve "unsized" (aka dynamically sized) types. However, that detail isn’t the most relevant, because it’s just yet-another reason why certain places can't support moving the value, but that kind of restriction is in no way unique to "unsized" types.


For further reading, I’d be happy if someone finds more beginner-friendly material; I can find the section in the reference which is however terse and technical; and e.g. this blog post which focuses on certain aspects and technicalities only, but at least is more meant to be read by humans.

3 Likes

Being able to call a method on a place without moving from behind the place can be demonstrated more directly (and without involving unsized types / without being slice specific):

fn example(hs: &mut HashSet<u32>) {
    // error[E0507]: cannot move out of `*hs` which is behind a mutable reference
    // *hs;

    // Compiles
    (*hs).insert(0);
}

Connecting it back to the OP, v[..] is notionally (*Index::index(&v, ..)), where index returns a &[i32]. That is, indexing results in a place just as dereferencing results in a place.

This is arguably just a combination of @steffahn's first couple snippets.

1 Like