taotao
taotao

Reputation: 171

Cannot add-assign within `Vec` of user-defined type

Consider this code (Rust Playground):

#[derive(Clone, Copy, Debug)]
struct X(i32);

impl std::ops::AddAssign for X {
    fn add_assign(&mut self, rhs: Self) {
        self.0 += rhs.0;
    }
}

fn main() {
    let mut ary_i32 = [1_i32; 2];
    ary_i32[0] += ary_i32[1]; // OK

    let mut ary_x = [X(1); 2];
    ary_x[0] += ary_x[1]; // OK

    let mut vec_i32 = vec![1_i32; 2];
    vec_i32[0] += vec_i32[1]; // OK

    let mut vec_x = vec![X(1); 2];
    vec_x[0] += vec_x[1]; // error[E0502]: cannot borrow `vec_x` as immutable because it is also borrowed as mutable
}

Why I get E0502 only on vec_x line? I could not understand why only the operations for ary_x and vec_i32 are permitted. Does borrow checker treat builtin types (i32, array) specially?

Upvotes: 10

Views: 822

Answers (2)

taotao
taotao

Reputation: 171

I researched some resources and read MIR of my code, and managed to understand what is going on.
The comment by @trentcl will be the best answer. I write the details as possible.

For array, Index and IndexMut traits are not used and compiler directly manipulates array elements (you can see this with MIR). So, borrowing problem does not exist here.

Explanating for Vec, rustc guide is useful.
First, Two-phase borrow is not applied to vec_foo[0] += vec_foo[1] statement.
And, the difference between i32 and X is caused by operator lowering.
Basically, statements like vec_user_defined[0] += vec_user_defined[1] are converted to function calls like add_assign(index_mut(...), *index(...)), and function arguments are evaluated from left to right. So, index_mut() borrows x mutably and index() tries to borrow x, and fails.
But for builtin types like i32, compound assignment operator is not converted to function call, and rhs is evaluated before lhs (you can see index() is called before index_mut() with MIR). So, for builtin types, vec_builtin[0] += vec_builtin[1] works.

I know these things from lo48576's article (Japanese).

I considered some workarounds:

  • Just use an intermediate variable as @sebpuetz said.
  • Convert Vec to slice as @trentcl said. But this doesn't work well for multidimensional Vec.
  • Write some macro to automatically introduce an intermediate variable. I found rhs_first_assign crate does such works.

Upvotes: 7

cryptograthor
cryptograthor

Reputation: 475

Rust arrays live on the stack, are predictably sized, and therefore have stronger borrow checker guarantees. Vectors are smart pointers on the stack pointing at data that can grow and shrink on the Heap. Because the final example uses the Vector type, the borrow checker considers the entire Vector as a single mutably borrowed object when loading it from the Heap.

As you've observed, the borrow checker can create a mutable reference to a single element to something living on the Stack, whereas it creates a mutable reference to the Vector's smart pointer on the Stack, and then a further mutable reference to the data on the heap. This is why the immutable reference to vec_vec_x[1][1] fails.

As @sebpuetz noted in a comment, you can solve this by first copying an immutable reference to vec_vec_x[1][1], then creating an immutable reference.

Upvotes: 1

Related Questions