2

I am new to Rust and I cannot solve this simple problem: Consider following code:

let mut a = vec![String::from("aa"), String::from("bb")];
a[0] += &*a[1];

Borrow checker rightfully complains about me having both immutable and mutable borrows here. It also suggests me:

help: try adding a local storing this...
  --> src\main.rs:61:15
   |
61 |     a[0] += &*a[1];
   |               ^^^^
help: ...and then using that local here
  --> src\main.rs:61:5
   |
61 |     a[0] += &*a[1];
   |     ^^^^^^^^^^^^^^

I do not really understand what that means. Do I really need to clone the string to perform such an easy operation? (That would result in 2 overall copies: into temporary and then back into a[0], instead of optimal 1 copy straight into a[0])

1 Answers1

3

Naively, like this (no reason to dereference a[1]):

fn main() {
    let mut a = vec![String::from("aa"), String::from("bb")];
    a[0] += &a[1];
}

Sadly, as you noticed, this creates a problem with the borrow checker. Modifying a[0] borrows it mutably, while you also need to borrow a[1] immutably to append it. And borrowing a both mutably and immutably at the same time isn't allowed:

error[E0502]: cannot borrow `a` as immutable because it is also borrowed as mutable
 --> src/main.rs:3:14
  |
3 |     a[0] += &a[1];
  |     ---------^---
  |     |        |
  |     |        immutable borrow occurs here
  |     mutable borrow occurs here
  |     mutable borrow later used here

The reason is that Rust doesn't know that a[0] and a[1] are independent variables. This behavior is intentional and important, as it prevents some cases that could lead to undefined behavior. Rust has a zero-undefined-behavior guarantee.

You can split the array into two independently mutable parts via the split_at_mut function, though. This works because Rust can now guarantee that both parts are independent. Borrowing them sequentially makes it impossible for (current) Rust to prove that.

fn main() {
    let mut a = vec![String::from("aa"), String::from("bb")];

    let (a_left, a_right) = a.split_at_mut(1);
    a_left[0] += &a_right[0];

    println!("{:?}", a);
}
["aabb", "bb"]

Note that the split_at_mut is almost free. It doesn't actually copy anything, it just creates two slices that point at two non-overlapping parts of the array. It doesn't cause any performance overhead.

Finomnis
  • 18,094
  • 1
  • 20
  • 27
  • Thanks, did not know about that hack. But, reducing to even simpler case, you can not have: `let mut a: String = "11".to_string(); a += &a;`? – nexus.chebykin Nov 27 '22 at 19:42
  • Also this becomes harder (if case required) when it is i and j instead of 0 and 1, is there any better way to do that? – nexus.chebykin Nov 27 '22 at 19:55
  • @nexus.chebykin *"is there any better way to do that?"* - You could use `RefCell` for interior mutability; but that would introduce a runtime overhead. – Finomnis Nov 27 '22 at 21:30
  • *"did not know about that hack"* - I don't think it's a hack. I think it's the intended way of borrowing exclusively from multiple places of the vector at once. *"when it is i and j instead of 0 and 1"* - Just use the proper offset in your `split_at_mut`. You can split at any position. – Finomnis Nov 27 '22 at 21:32
  • "`let mut a: String = "11".to_string(); a += &a;`" - this would be undefined behaviour in most programming languages. Rust is just the only language that complains about it; as I already mentioned, Rust by definition has zero undefined behaviour. – Finomnis Nov 27 '22 at 21:34
  • _"You can split at any position"_. Yes, no wonder. But it requires if checking whether i < j or i > j and looks ugly taking around 10 lines instead of 1... – nexus.chebykin Nov 28 '22 at 07:25
  • _"This would be undefined behavior in most programming languages"_ For example? In C++ It is perfectly fine – nexus.chebykin Nov 28 '22 at 07:26
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/249939/discussion-between-nexus-chebykin-and-finomnis). – nexus.chebykin Nov 28 '22 at 07:31
  • I think you are mistaken about C++ ... `std::string::append` and `std::string::operator+=` both create a temporary copy, and `strcat` explicitely forbids that the target overlaps with the source. – Finomnis Nov 30 '22 at 21:29
  • No, string.append does not create temporary – nexus.chebykin Dec 01 '22 at 08:37
  • @nexus.chebykin https://stackoverflow.com/a/37075341/2902833 No, it doesn't create a temporary copy, but it still copies the original string instead of appending to it. Which is not how Rust behaves, Rust's `append` behave more like a combination of `strcat` combined with a reallocation in case the string grows larger than capacity. – Finomnis Dec 01 '22 at 22:47
  • Either way, you have to choose your evil. There are only two ways to concatenate a string: via performing one extra copy, which allows appending a string to itself (as in C++/Python/Java), or via modifying the original string in place (like in C or Rust), which doesn't allow appending a string to itself. This problem is closely related to why it isn't legal to modify an array whilst iterating over it, in case you ever stumbled across this. – Finomnis Dec 01 '22 at 22:50