Exclusive access during method call, but shared access after return

I have a method (specimens), which is supposed to sort an inner Vec and return a shared read-only reference to the sorted Vec/slice. However, this means that for the lifetime of the read-only reference, there is a mutable borrow from the outer structure alive.

Is there any way around this? I feel like it could be a common problem when a method requires mutable access during runtime, but only shared access after having returned?

What is the best way to deal with these sort of situations? Should I think about an entirely different API?

#[derive(Default)]
pub struct Foo {
    meta: i32,
    is_sorted: bool,
    specimens: Vec<String>,
}

impl Foo {
    fn meta(&self) -> i32 {
        self.meta
    }
    fn specimens(&mut self) -> &[String] {
        if !self.is_sorted {
            self.specimens.sort(); // needs &mut self
            self.is_sorted = true;
        }
        &self.specimens
    }
    fn specimens_prepare(&mut self) {
        if !self.is_sorted {
            self.specimens.sort(); // needs &mut self
            self.is_sorted = true;
        }
    }
    fn specimens_doit(&self) -> &[String] {
        if !self.is_sorted {
            panic!("use `specimens_prepare` first");
        }
        &self.specimens
    }
}

fn main() {
    let mut foo = Foo::default();
    let specimens = foo.specimens();
    // `specimens` is borrowed exclusively, so the following fails to compile:
    //let _ = foo.meta();
    drop(specimens);
    
    // But we could do:
    foo.specimens_prepare();
    let specimens = foo.specimens_doit();
    let _ = foo.meta();
    drop(specimens);
}

(Playground)

Not directly; we'd need some sort of new annotation to reflect that the &mut should be reborrowed as & in concert with the value being returned.

Probably. I think your example is just an example so it's hard to give advice; you don't need the functionality in the example (copy out meta first). But you could return something containing both &[String] and &i32 say.

Perhaps take inspiration from entry APIs, if they suit your actual use case.

2 Likes

You can explicitly return an immutable reference to self:

fn specimens(&mut self) -> (&Self, &[String]) {
    if !self.is_sorted {
        self.specimens.sort(); // needs &mut self
        self.is_sorted = true;
    }
    (self, &self.specimens)
}

In main, it would look like this:

let mut foo = Foo::default();
let (foo, specimens) = foo.specimens();
// this works
let _ = foo.meta();
drop(specimens);

But I think it would be more idiomatic to take and return by value.

2 Likes

For that matter you can return something that contains both the &[String] (presumably something more complicated in the real use case) and &Self, and then implement methods on that or even expose the &Self. Basically a manual version of the hypothetical annotation.

I've used this sort of pattern, not for &mut downgrading, but in some DAG structure wherein edges were keys and the root data structure held a store of all the nodes, so if you wanted to access a node's children nodes say, you needed to query the root. It was nicer just to have a NodeHandle<'a> (that had a reference back to the root) which could return an iterator of other NodeHandle<'a> or whatever via method call.

1 Like

So I guess this has been discussed before? To me it seems like a reasonable feature, though I'm not sure how frequent is this problem.

Oh, very nice! In my case it would be a bit noisy/ugly, but I like that it's at least possible to do this (if I really need it).

Why not using std::ops::Deref for that matter? See example below:

#[derive(Default)]
pub struct Foo {
    meta: i32,
    is_sorted: bool,
    specimens: Vec<String>,
}

pub struct SortedFoo<'a> {
    foo: &'a Foo,
}

impl std::ops::Deref for SortedFoo<'_> {
    type Target = Foo;
    fn deref(&self) -> &Foo {
        &self.foo
    }
}

impl Foo {
    fn meta(&self) -> i32 {
        self.meta
    }
    fn sorted(&mut self) -> SortedFoo {
        if !self.is_sorted {
            self.specimens.sort(); // needs &mut self
            self.is_sorted = true;
        }
        SortedFoo { foo: self }
    }
}

impl SortedFoo<'_> {
    fn specimens(&self) -> &[String] {
        // we know these are sorted
        &self.specimens
    }
}

fn main() {
    let mut foo = Foo::default();
    {
        let sorted_foo = foo.sorted();
        let specimens = sorted_foo.specimens();
        let _ = sorted_foo.meta();
        drop(specimens);
    }
}

(Playground)

This basically circumvents the panic here:


SortedFoo is now some sort of pointer here: a pointer with a guarantee. It guarantees that the pointed-to Foo is sorted. Thus I feel like implementing Deref is idiomatic.

But I'm overall not sure if I need this in my real-world code. Speaking in terms of the example, I might just require reading the meta beforehand. Still curious though, if the SortedFoo approach seems generally reasonable/idiomatic.

1 Like