Why does `let x = v[0]` move?

Consider this code snippet:

fn main() {
    let v = vec!["hello".to_owned()];
    let x = v[0];
}

Now, according to #place-expressions-and-value-expressions:

  • The array index expression v[0] is a place expression;
  • The initializer of a let statement is a place expression context.

Thus in let x = v[0], v[0] is a place expression evaluated in a place expression context. So why does it move?

In fact I'm having trouble understanding why the initializer of a let statement is a place expression context. It seems that since the initializer is on the right hand side of the equal sign, it should be a value expression context.

Thanks!

1 Like

This is a result of how Rust desugars the [] syntax into calls to std::ops::Index.

The compiler converts v[0] into *v.index(0), note the * to immediately dereference the reference returned by Index::index(), so that the resulting expression will evaluate to what you expect (i.e. indexing into a Vec<usize> will give you a usize).

This is why the documentation says indexing will return the "place" (i.e. reference) of the item you're trying to access. If the Index trait didn't always return a reference, you'd have a tricky situation where every vector access will return the item by value... which is a move, and would leave the place it used to be in logically uninitialised.

For Copy types this works exactly as expected because the compiler automatically makes a copy for you. However in types that aren't Copy this means you'll be dereferencing a pointer to something you don't own. The solution is to immediately "re-borrow" the item (i.e. &v[0]) so it is always behind a reference.

To think about it another way here's something similar, except we're using a struct and explicitly calling getters instead of using the [] syntactic sugar.

struct Person {
    name: String,
    age: u32,
}

impl Person {
    /// Getting a `Copy` type by "indexing" into our struct.
    fn get_age(&self) -> &u32 {
        &self.age
    }
    
    /// Doing the same for the non-`Copy` type.
    fn get_name(&self) -> &String {
        &self.name
    }
}

fn main() {
    let person = Person { name: String::from("michael"), age: 42 };
    
    let age = *person.get_age();
    let name = *person.get_name();
    let reborrowed_name = &*person.get_name();
}

(playground)

Hitting the playground's "build" button shows us this compile error:

error[E0507]: cannot move out of a shared reference
  --> src/main.rs:22:16
   |
22 |     let name = *person.get_name();
   |                ^^^^^^^^^^^^^^^^^^
   |                |
   |                move occurs because value has type `std::string::String`, which does not implement the `Copy` trait
   |                help: consider borrowing here: `&*person.get_name()`

error: aborting due to previous error

Does that make sense as to why let x = v[0] will move the value?

2 Likes

Thanks for the detailed explanation! I think your reply mainly clears that since the index expression v[0] can be desugared into *v.index(0), i.e. a dereference expression, which is itself a place expression, so index expression being place expression is just a special case of dereference expression being place expression.

However my main confusion is that the documentaion states that a value will be moved/copied when:

When a place expression is evaluated in a value expression context ... the value will be copied ... move ...

Since in let x = v[0] , v[0] is a place expression evaluated in a place expression context, (not in a value expression context), it does not fit the condition of a copy/move.

Now I do see that there is a second condition "... or is bound by value in a pattern". However as a non-native English speaker, I'm not sure what it means. Does it mean the place expression being considered is in the pattern or being matched by a pattern?

Check the compiler error message again. It says

error[E0507]: cannot move out of index of `std::vec::Vec<std::string::String>`

We cannot move it out since it doesn't fit the condition. But we should move it out as it's a place expression evaluated in a value expression context. So it fails to compile.

2 Likes

I'm guessing it's talking about this sort of code:

struct Person {
    name: String,
}

fn main() {
    let people: Vec<Person> = Vec::new();

    let Person {
        name: ref name_of_first_person,
    } = people[0];
}

While we're using people[0] which would normally move the value, we're immediately using pattern matching to get a reference to the person's name field so no moving actually needs to be done.

To be honest, I hadn't really thought this deeply about how indexing works in Rust before... I guess I stumbled my way through early on, built up an intuition for how it works after being yelled at by the compiler, and never stopped to think about the formalities and edge cases :stuck_out_tongue_winking_eye:

Thanks for asking the question, it was a neat little rabbit-hole to go down!

3 Likes

Thanks for replying! However according to #place-expressions-and-value-expressions, the initializer of a let statement is a place expression context.

(Which is another big confusion to me. Intuitively I think it's a value expression context as well)

Yes that's what I thought as well, since the syntax of let is let pattern = initializer expression, the statement let x = v[0] is actually introducing a binding by matching the identifier pattern x against the initializer.

So, it seems that the second condition "... or is bound by value in a pattern"
should be expanded into:
".. or is being matched against by an identifier pattern which is in value binding mode."

A big reason I chose to learn Rust is that it forces me to think about many basic concepts deeper than in other languages :stuck_out_tongue_winking_eye:

1 Like

"bound by value in a pattern" pretty clearly seems to me to be talking about the let x = ... case (and other situations where the value has to be moved). What else could it mean? The alternative, "being matched against by an identifier pattern which is in value binding mode", seems (to me) just an extra wordy way of saying "bound by value in a pattern".

You could also bind by reference in a pattern, as @Michael-F-Bryan also mentioned. In the simplest possible case:

let ref x = v[0];
2 Likes

Many thanks for confirming my guess! I guess this time my English fails me. :stuck_out_tongue_winking_eye:

Also could you shed some light on why is the initializer of a let statement a place expression context? It seems both I and @Hyeonu think that it seems more like a value expression context.

If it were a value expression context, let ref x = v[0] wouldn't work.

In a sense you could say that whether a let has place context or value context depends on the pattern:

let x = foo;     // foo has value expression context because it is bound by value
let ref x = foo; // foo has place expression context because it is not bound by value
let _ = foo;     // foo has place expression context because it is not bound at all

However, that gets complicated when you match against multiple things which have different binding modes:

// suppose foo: (i32, String)
let (x, y) = foo;         // ok, foo has value expression context
let (ref x, ref y) = foo; // ok, foo has place expression context
let (x, ref y) = foo;     // foo.0 is copied to x, so it has value context,
                          // but foo.1 is referenced, so it has place context?
//let (ref x, y) = foo;   // does not compile today
1 Like

Thanks for the examples! It seems to me that your post mainly states that place or value context depend on the "result" of a bind, not a set of rigid rules. However this seem to contradict what the Rust reference said.

The problem for me is that I don't know the precise meaning of "place/value expression context". I was not able to find a description or definition in the Rust reference.

I think the confusion I caused in saying "depends on the pattern" is what the reference is trying to avoid by saying

is evaluated in a value expression context, or is bound by value in a pattern

i.e., it's not that the context changes from place-expression to value-expression, but that binding by value, despite happening in a place expression context, follows the same rules as other kinds of moves that happen in a value expression context, such as {expression} or foo(expression), etc.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.