Rust Compilation Error: cannot borrow as mutable because it is also borrowed as immutable

Hello, I'm new to rust, and I'm trying to compile this code:

struct ObjA {
    v: usize,
    ptr: *const [i32]
}

impl ObjA {
    fn get_ptr(&self) -> & [i32] {
        unsafe { &*self.ptr }
    }
}
struct ObjB<'a> {
    a: &'a [i32],
    obja: &'a mut ObjA
}

fn main() {
    let mut buf = [0; 10];
    let mut obja = ObjA { v: 12, ptr: buf.as_slice() };

    let ptr = obja.get_ptr();
    let b = ObjB { a: ptr, obja: &mut obja };

}

error message:

error[E0502]: cannot borrow `obja` as mutable because it is also borrowed as immutable
  --> src/main.rs:21:34
   |
20 |     let ptr = obja.get_ptr();
   |               -------------- immutable borrow occurs here
21 |     let b = ObjB { a: ptr, obja: &mut obja };
   |             ---------------------^^^^^^^^^--
   |             |                    |
   |             |                    mutable borrow occurs here
   |             immutable borrow later used here

I don't understand why the immutable borrow is still used after get_ptr function has been executed.

The immutable borrow lasts until the last use of the return value of get_ptr because the un-elided form of your function is:

impl ObjA {
    fn get_ptr<'a>(&'a self) -> &'a [i32] {
        unsafe { &*self.ptr }
    }
}
1 Like

First up, note that it's usually a good idea to learn some Rust without immediately using unsafe code. E.g. the thing you're doing here with obja which contains a pointer to the stack to buf: You can do that with

struct ObjA<'a> {
    v: usize,
    ptr: &'a [i32]
}

too. But that naturally leads to the second problem: usually it's a good idea to avoid putting references into your structs when first learning Rust. There are valid use cases for something like this, but a lot of times what you actually want is that your structs own their field values. This is way more flexible; you can freely pass around such structs then without being constrained by whatever owns the stuff the struct's field borrows from being still alive.

To explain the error message: ObjA::get_ptr is a function that converts/projects a borrow. It turns an immutable borrow of the whole ObjA into an immutable borrow of the [i32] it points to. The function's signature is what's important here: fn get_ptr(&self) -> &[i32] is a signature with so-called "elided" lifetimes, meaning that it's a shorthand for fn get_ptr<'a>(&'a self) -> &'a [i32] which couples the lifetime of the borrow of self to the lifetime of returned reference. If the [i32] was owned by ObjA, or ObjA would contain a mutable borrow, then that's all that would be possible; however if it's supposed to be a shared borrow of the [i32], then you can actually change the signature and make get_ptr return a longer-lived reference than what the lifetime of the &self parameter is; let me give some code examples:

First addressing the use of raw pointers, raw pointers like *const [i32] can really "mean" anything, so there's a decision whether to use &, or &mut or Box or perhaps Rc/Arc as a safe alternative. Assuming &, i.e. shared references, you could use

struct ObjA<'a> {
    v: usize,
    ptr: &'a [i32]
}

and then you can change the lifetimes on get_ptr so that it's

impl<'a> ObjA<'a> {
    fn get_ptr(&self) -> &'a [i32] {
        self.ptr
    }
}

Note how the return type &'a [i32] no longer couples its lifetime to the &self. Next up, we need to change ObjB accordingly

struct ObjB<'a, 'b> {
    a: &'a [i32],
    obja: &'b mut ObjA<'a>
}

and the code works now: Rust Playground

However, we are immediately deep into having way too many lifetimes on out structs; it's important to evaluate whether we want some owned fields after all. Before that, there's the question of why the &'a [i32] is duplicated; is that something that's necessary? Of course that would depend on the us case; this is kind-of some dummy code only, AFAICT; but if the obja field of ObjB would always keep containing the same ptr as the a field, then we unnecessarily duplicated that pointer. Without such duplication, everything could become owned instead, e.g. Rust Playground. As mentioned, owned data is always easier to work with.

If you want to keep the shared [i32]; perhaps because the b.obja field is supposed to change over time, there's still opportunity to at least make the obja field owned. Simplifying

struct ObjB<'a> {
    a: &'a [i32],
    obja:ObjA<'a>,
}

Rust Playground

This has the nice side-effect that ObjB now at last does not have multiple lifetime arguments anymore. Couldn't we avoid the multiple lifetime arguments like this, too?

struct ObjB<'a> {
    a: &'a [i32],
    obja: &'a mut ObjA<'a>,
}

Well, technically, yes, but that's a bad idea: The use of the same lifetime on multiple levels as in &'a mut ObjA<'a> is usually an antipattern. It's incredibly inflexible to work with; the obja field will need to contain a borrow of the ObjA<'a> essentially for the entire duration of its existence anyways, so an owned argument seems better. This pattern can sometimes be okay with a shared reference, so &'a ObjA<'a> can be fine, but with a mutable reference like &'a mut ObjA<'a>, it's basically always something you should avoid.

Finally note, that if you want the benefits of avoiding all the lifetime parameters on your structs, while still sharing the [i32], you could consider using a reference-counted pointer. E.g.

struct ObjA {
    v: usize,
    ptr: Rc<[i32]>,
}

struct ObjB {
    a: Rc<[i32]>,
    obja: ObjA,
}

Rust Playground

Unlike &'a [i32], Rc<[i32]> doesn't support slicing though (so you couldn't cheaply replace b.obja.ptr with a subslice of itself, something you can do with the &'a [i32] approach), and has a very minimal runtime-cost from counting the references. It's obviously all a trade-off, there's not a clear best solution in a toy example like this :slight_smile:

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.