Is indexing into a vec a borrow or a move?

This piece of code:

#[derive(Debug)]
struct Age {
    value: u32
}
fn main() {
  let ages = vec![Age {value: 1}, Age {value: 2}];
  let age = ages[0];
  println!("{:?}", age);
}

fails to compile with the following error

Compiling playground v0.0.1 (/playground)
error[E0507]: cannot move out of index of Vec<Age>
--> src/main.rs:7:13
|
7 | let age = ages[0];
| ^^^^^^^
| |
| move occurs because value has type Age, which does not implement the Copy trait
| help: consider borrowing here: &ages[0]
For more information about this error, try rustc --explain E0507.
error: could not compile playground due to previous error

I was not expecting this to be the case. I was not expecting indexing into a vec to be a move, but looking at the error, it seems that is what it is.

An explicit reference compiles though:

#[derive(Debug)]
struct Age {
    value: u32
}
fn main() {
  let ages = vec![Age {value: 1}, Age {value: 2}];
  let age: &Age = &ages[0];
  println!("{:?}", age);
}

Any idea why this is the case? Is indexing into a vec actually a move and I was wrong about it being a borrow?

1 Like

An indexed element is a place expression (other languages call this an "lvalue"). Thus, it's a value, so it can be moved, but it also makes sense to borrow it.

The expression arr[index] actually desugars to

*Index::index(&arr, index)

Note the explicit dereference. This is how Rust emulates a place expression in indexing – Index::index() returns a pointer, and then it's immediately dereferenced. Thus, if you simply index, that's a move, and if you take a reference to it, that yields

&*Index::index(&arr, index)

where referencing and dereferencing cancel out, and thus you get a reference, exactly as intended.

8 Likes

Also see:

Array and slice indexing expressions

[…]

For other types an index expression a[b] is equivalent to *std::ops::Index::index(&a, b), or *std::ops::IndexMut::index_mut(&mut a, b) in a mutable place expression context. […]

1 Like

Still yet to fully understand the explanation.

But maybe something that will help is to use the example I posted? Based on your explanation is it possible to relate it to the example to show exactly why it was not compiling? Why a move was being attempted which leads to compilation failure? This will be appreciated.

It's an interesting discovery that I didn't think about either.

I think the reason it's a copy by default is because you can pass it then to a function and move it, leaving the slot in the vec empty (and leaving undecided what you need to do with the empty slot).

fn another_func(age: Age) { println!("{:?}", age); }
...
let age = ages[0]; // take ownership
another_func(age); // pass it elsewhere, and it is destroyed
// what's in ages[0] now?!

That's why it must be copied.

If it were reference by default, this would probably be unclear when you read the code.

Be careful with terminology here. I don’t know what exactly you mean by “thus, it’s a value” but to me “value” sounds too much like “value expression”, so this feels like inappropriate terminology to use for a “place expression”.

Expressions are divided into two main categories: place expressions and value expressions.
from: The Rust Reference

oh wait...so the correct way to view it is that, indexing is not a borrow, neither is it a move, but it is a copy?

The best example for “place expressions” are local variables. If you have

let x = String::from("Hello");

then the expressionx” refers to the place x. Simply writing x in your code doesn’t do anything in particular to x yet…

let x = String::from("Hello");
match x { _ => {} } // <- we wrote `x` here
println!("{x}"); // x ist still there. We also wrote `x` here by the way…
println!("{x}"); // x ist still there. Everything compiles.

We can use the place expression x in many ways that won’t move x. The most typical example is creating a reference, e.g. &x. This will borrow x, not move it. Similarly, the desugaring of println only borrows x, and a match statement might borrow, borrow mutably, move, partially borrow or move, or even do nothing at all to the expression you pass to it, depending on what patterns you do or do not match against.

Now if we write

let y = x;

then x gets moved. It’s not like “a move is attempted”, it’s more like “we definitely and quite explicitly move x” here. And it’s the same thing with

let age = ages[0];

which explicitly moves ages[0]. Just like x above was a place expression of type String, ages[0] is a place expression of type Age. We can borrow it or borrow it mutably to create &Age or &mut Age values via &ages[0] or &mut ages[0]. Unlike x, we are not allowed to move ages[0] though, since indexing in Rust doesn’t support by-move access at all. (Hence the error message. We do move, yet we are not allowed to.) This can happen for variables, too, but for different reasons. Typical examples of when you cannot access x: String from the example above by-move include

When it’s borrowed

let x = String::from("Hello");
let r = &x; // borrows `x`
let y = x; // here’s the move
println!("{r}"); // r is still active until this point

Or when it’s no longer initialized

let x = String::from("Hello");
drop(x); // moves out of `x`
let y = x; // here’s the move

both examples above fail to compile, one with an error saying cannot move out of `x`… and one use of moved value: `x` . The former would still work if x was accessed by mutable reference instead; the later can’t be borrowed either, and may only be accessed as the left-hand-side of an assignment that re-initializes the variables x = …new_value…;.[1]


  1. which would then also require to mark x as mutable, but that’s a minor change I’m overlooking here ↩︎

4 Likes

as steffahn wrote above, assigning is a move (but copy with vec items).

Actually… not quite. It desugars either to *Index::index(&arr, index) when the place is accessed by immutable borrow (or being copied) and to *IndexMut::index_mut(&mut arr, index) if it is accessed by mutable borrow.[1] If it is moved then an error message is created (the very error in question here), arguably before the thing is desugared at all.


  1. Plus, there’s something extra going on with regards to auto-deref, so it’s in a sense somewhat more similar to a method call *arr.intex(index) (I’m not certain whether it’s fully equivalent to such a method call, ignoring the fact that the trait might not be in scope, or whether there’s still subtle differences). ↩︎

As I’m arguing above, assigning always moves. Well, except for T: Copy types, all move operations are actually copy operations that don’t invalidate the moved-out-from value. And vec items don’t behave any differently, except they can’t be actually moved, though copying (i.e. for T: Copy types) is still allowed.

1 Like

That’s certainly an interesting design question why it behaves the way it does. In my mind, the benefit of the approach we take in Rust is that we can have a unified indexing-syntax that can allow you to do both immutable or mutable access; we save the need for the otherwise omnipresent pattern of needing different methods for different things.

Possibly more importantly probably, the behavior – the way it is – mirrors more closely the precedent given by C and C++. So if you have an array of numbers, a: [i32; 100], you can use C-like indexing syntax to read values

let n: i32 = a[42]; // `a[42]` already is an integer, not a pointer/reference to an integer

or modify them

a[42] = 10;
a[42] += 1;

In particular, there’s no need to write a * (dereferencing) above.

(It also makes indexing behave similarly to field accesses (let n = foo.bar; or foo.bar += 1;); just like indexing field accessors also operate on place expressions and then are place expressions.)

Compare this to the case of having a reference r: &mut i32 where you do write a * it in all these cases

let n: i32 = *r;
*r = 10;
*r += 1;

which makes r behave syntactically comparable to pointers in C/C++, (and different from “references” in C++), also a deliberate choice.

1 Like

It's because in C that's just a syntax sugar for *(a + 42) += 1. Literally.

Dereference is most definitely there, and because + is commutative indexing operation is commutative in C.

You can write 42[a] += 1, too. It's just matter of taste.

I don't think any other language have it commutative. Rust and C++ certainly don't subscribe under that idea.

Again, because arr[index] by itself refers to the indexed item itself, it's not a reference to the indexed item (unless you add the &). When you pass around values, they get moved by default.

Yeah, I'm aware, but no mutation is being attempted here.

I'm not sure how useful it is to argue about the order of the creation of the error message and the desugaring; it would work exactly in the same way in both cases (because it is not allowed to just move out of a reference, either).

No.

  • let other_value = arr[index]; is a move.
  • let other_ref = &arr[index]; is a shared borrow.
  • let other_ref_mut = &mut arr[index]; is a mutable borrow.

And, independently, a move for a Copy type is a copy. Thus, let other_value = arr[index]; is a copy if and only if the item type is Copy.

4 Likes

ok so to answer the question in the title of this post. Indexing into a vec (and passing that to a variable), is a move.

Indexing and passing to variable are two different things.

a[index] += 1; is not a move, most definitely.

Yeah. Seems indexing by itself is just that. And If I am not misunderstood, from the discussion in this thread, an indexed vec by itself is a place expression. It is the assigning that makes it a move.

1 Like

No, the point is exactly that you can't tell by itself what it is, you have to look at the context.