Allow reading of a mutable reference after it's reborrowed as immutable


struct Foo{
    name: String,
    score: u32
}

impl Foo{
    fn new(id: String, score: u32) -> Foo{
        Foo { name: id, score: score }
    }

    fn add_score(&mut self) -> &u32 {
        self.score += 1;
        &self.score
    }

    fn get_name(&self) -> &String {
        &self.name
    }
}

fn main() {
    let mut foo = Foo::new("foo".into(), 0);
    let score = foo.add_score();
    let name = foo.get_name();
    println!("{}: {}", name, score);
}
error[E0502]: cannot borrow `foo` as immutable because it is also borrowed as mutable
  --> src/bin/me.rs:25:16
   |
24 |     let score = foo.add_score();
   |                 --------------- mutable borrow occurs here
25 |     let name = foo.get_name();
   |                ^^^^^^^^^^^^^^ immutable borrow occurs here
26 |     println!("{}: {}", name, score);
   |                              ----- mutable borrow later used here

Because add_score method take s a &mut self, the foo is regared as borrowed as mutable as long as the returned immutable ref has not gone out of scope, thus making both reading and writing of foo rejected. To word around this, we have to provide another function get_score, and call get_score after add_score to get the updated score instead of letting add_score return the updated score directly. It is a little cumbersome. In fact, the add_score method only returns an immutable ref of a Foo's field, it should be safe to call Foo's method that requires a &self on foo afterwards.

So after a mutable ref is reborrowed as immutable, is it reasonable to regard the reborrowed mutable ref as a immutable ref, and allow reading of it instead of rejecting both reading and writing? After all reborrows are out of scope, the beborrowed ref can recover to a mutable ref. If this can be done, the follwing code will compile too.

fn main() {
    let mut foo = Foo::new("foo".into(), 0);
    let foop = &mut foo;
    let score = foop.add_score();
    let name = foop.get_name();
    println!("{}: {}", name, score);
}

While this works in this case (and the idea of a "downgrading" borrow has been proposed before), it doesn't work in a general case just by looking at the function signature. The function return could have access to e.g. a mutex storing the input lifetime with write access.

2 Likes

But a function takes &self could also return a mutex or refcell too. If someone is not familiar with mutex or refcell, even if the function signature takes &mut self,he could not tell that the returned immutable ref could accidently modify the referenced value, right?

You're making the assumption that the shared reference could be created from both a shared or an exclusive reference, this is not always the case, sometimes creating a reference is only possible because you have exclusive access. For example Cell, RefCell, Mutex and RwLock only allow you to get an unguarded reference to the inner value if you have an exclusive reference to them because this guarantees that there's no other shared reference to them existing while the reference to the inner value is alive. If your proposal were to be accepted then they would become unsound. Consider for example:

let mut cell = std::cell::Cell::new(Box::new(0));
let exclusive_ref = cell.get_mut();
let immutable_ref = &*exclusive_ref;
let immutable_ref_to_inner = &**immutable_ref; // This points to the contents of the Box
// exclusive_ref is no longer user after this, only immutable_ref_to_inner,
// so it should be fine to have a shared borrow of cell, right?

// this takes &self, so we're good, we don't break the shared borrow of immutable_ref_to_inner
cell.set(Box::new(1));

// Now the original Box has been dropped, but immutable_ref_to_inner still points to its contents!
println!("Uh oh: {}", immutable_ref_to_inner); // this is an use-after-free
5 Likes

Despite the name, &mut means exclusive access, not mutable access, per se. Unsafe code, etc, is allowed to rely on this.

trait MutexExt<T> {
    fn lockless_borrow(&mut self) -> &T;
}

impl<T> MutexExt<T> for Mutex<T> {
    fn lockless_borrow(&mut self) -> &T {
        // Note that this does not lock as `&mut` indicates a unique borrow
        // https://doc.rust-lang.org/std/sync/struct.Mutex.html#method.get_mut
        self.get_mut().unwrap()
    }
}

fn pain(mut foo: Mutex<Foo>) {
    // By your proposal, `foo` is only immutably borrowed after this call
    let borrow_one = foo.lockless_borrow();
    // And as there is no lock, this will succeed
    let mut lock = foo.lock().unwrap();
    let borrow_two = lock.deref_mut();
    // And again by your proposal, `borrow_one` is still live here
    // But that's UB since `borrow_two` is a live `&mut` here
}

(Basically the same as @SkiFire13's example.)

That explanation is excellent, that convinced me. It really should be put in the book or somewhere.

But I'm still wondering, if we remove the get_mut method from all data structure that have interior mutability, will it be fine with this proposal? In other words, is the get_mut necessary in some situation? Is there any other thing that rely on the &mut's exclusive meaning other than get_mut?

2 Likes

There’s also methods like Cell::from_mut

impl<T: ?Sized> Cell<T> {
    pub fn from_mut(t: &mut T) -> &Cell<T>
}

that take a mutable reference as parameter and return some kind of immutable reference. In this case, it’s crucial, too, that the mutable borrow can’t be “downgraded” to an immutable one after the call to Cell::from_mut while the resulting &Cell<T> is still live.

1 Like

I think what would be necessary for writing the kind of code you want to get working would be some new kind of “downgradable mutable reference”. I’d imagine a type with two lifetime arguments, one (longer) for the duration of immutable borrow, and one (shorter) for the duration of mutable borow.

This could possibly even be a generalization of the existing &'a mut T type, which would be the special case of both lifetimes being the same. Assume the new type is for example written as &'a mut<'b> T, where 'a is the (longer) duration of immutable borrow, and 'b is the (shorter) duration of mutable borrow, and &'a mut T is a shorthand of &'a mut<'a> T, then your code would work if add_score is re-written as

impl Foo {
    fn add_score<'a, 'b>(&'a mut<'b> self) -> &'a u32 {
        self.score += 1;
        &self.score
    }
}

Or with some lifetime elision,

impl Foo {
    fn add_score<'a>(&'a mut<'_> self) -> &'a u32 {
        self.score += 1;
        &self.score
    }
}

Perhaps, the existing lifetime-elision rules that favor the lifetime of &self or &mut self would be extended to also favor the first/outer/longer/immutable lifetime 'a in &'a mut<'b> self, then the same thing would be written

impl Foo {
    fn add_score(&mut<'_> self) -> &u32 {
        self.score += 1;
        &self.score
    }
}
7 Likes

Removing those would be a major breaking change, so it couldn't happen before Rust 2.0 (ETA: None planned, perhaps never). Even if Rust and stdlib had that freedom, it would be a breaking change to a guarantee that is likely to be depended on by unsafe code, or even safe code that needs to uphold logical invariants. Both of those wouldn't have the relative benefit of code no longer compiling after the change, but would instead become silently buggy or even UB. I.e. it would be a particular insidious breaking change.

The insidiousness is moot though, as Rust and stdlib do not have the freedom to make breaking changes of this sort. So this would need to be a new feature, distinct from a plain fn(&mut A) -> &B.

There has already been a proposal with the same request. TL;DR: it can't be done. Here's why. It's explained in The Nomicon, too.

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