Lifetimes with mutable reference to self in function call


#1

I can’t believe I couldn’t find a prior question on this, so apologies if it’s a duplicate. The ones suggested by the interface all assume that the mutable reference exists on a different struct, but I’m just trying to mutate self with otherwise immutable references. I even feel like I saw this question asked and answered on stack overflow before, but I can’t find it!

I put a minimal repro here, but since it’s pretty short, I’ll include it as well:

struct A<'a> {
    string_ref: &'a String,
}

impl<'a> A<'a> {
    fn change(&mut self, val: &'a String) {
        self.string_ref = val;
    }
}

struct B<'a> {
    a: A<'a>,
    to_add: String,
}

impl<'a> B<'a> {
    fn change(&'a mut self) {
        self.a.change(&self.to_add);
    }
}

fn main() {
    let a = A {
        string_ref: &"hello".to_string(),
    };
    let mut b = B {
        a,
        to_add: "b".to_string(),
    };
    b.change();
    b.change();
}

The compiler error is on the very last line, b.change(). The error is Cannot borrow 'b' as mutable more than once at a time".

I understand that I’m forcing the reference to &mut self to live way too long. So what I don’t understand, is how to avoid it! I tried this code for the B::change method (based on some of the questions here), but ended up with the same error:

    fn change<'b: 'a>(&'b mut self) {
        self.a.change(&self.to_add);
    }

For greater context, in my production code, I’m creating a facade where a struct like A in my code has several methods that accept a reference to a value. A new wrapper struct, like B above, stores that struct and a value that is passed into those methods for each of them.


#2

The short answer is: don’t use references inside structs, and all your problems will go away.


First, &String is a very unusual type — you require the data to be stored as a growable string, somewhere else, but you also forbid the string from growing by locking it with a read-only shared borrow. Unless you’re really really certain you need that, at cost of double indirection to have a thin pointer instead of a fat pointer, it’s probably a mistake.

You either want &str for temporarily borrowed strings, or owned String to store strings permanently (in both cases the string data is stored by reference). In structs it’s almost always String.


Nothing good ever comes from putting a lifetime on &self. If you ever find yourself doing that, take two steps back, because it’s a dead-end: <'a> on a struct it means the struct depends on something else that has been created earlier, before that struct. &'a self means self has to live at least as long as that other thing.

&mut self requires exclusive access to self for duration of the call. &'a mut self requires exclusive access to self for as long as lifetime of 'a. So b.change() locks b for exclusive access for as long as variable a exists.


And none of that can work, because you’re creating a self-referential struct. Rust reserves right to move owned structs by copying them to a new address, and move invalidates all references.

You can’t have a struct that contains both to_add as owned value, and a reference to &to_add. That’s because when B is moved to another address, the address of to_add changes, so all references to &to_add become invalid, so that would mean A is always unsafe to move to another address.

You either have to have A and B as separate structs (so that Rust can mark B unmovable for as long as A has a borrow of to_add), or you could make it:

A {
  string: String
}
B {
  a: A, 
  to_add: Option<String>,
}

and assign b.a.string = b.to_add.take().unwrap();


#3

@kornel already mentioned the self-referential struct aspect, but I wanted to ask: do you need String? Or can you use Rc<str>? If you can, then you can share ownership that way, like this playground example using your distilled A and B.


#4

Thank you for the extremely clear explanation. I didn’t realize I was building something self-referential. Now that I see it, it’s obvious it won’t work.

I’m not using strings in my code; I just through those in there as an example of something that is non-copyable. My production code uses a struct of my own devising. Unfortunately, I keep a lot of references to that struct. I should move everything around, but what I probably will do is make B.a a mutable reference and B.to_add a reference itself. But I expect to regret that. Whenever I’ve tried to put a mutable reference on a struct in the past, I’ve always regretted it…


#5

Yep, which goes back to the very first thing Kornel said. And I tend to agree with this sentiment. It’s much easier to write (and reason about) code that prefers ownership over referencing. On the other hand, this style trades off some performance by requiring Clone in some cases.

Rc and friends can workaround that limitation, if necessary. See: https://doc.rust-lang.org/book/ch15-04-rc.html But then you have to beware of creating reference cycles (memory leaks).


#6

For future reference: after a good night’s sleep, I solved my immediate issue in a couple minutes here

The only major change was to make to_add a reference:

struct B<'a> {
    a: A<'a>,
    to_add: &'a String,
}

In this particular case, my goal was to “write the new class B without changing the class A”. Pushing the ownership of the to_add variable onto the client solved it in this case.


I wanted to ask a bit more about the language philosophy. I am primarily a Python developer, where one can think of “everything is a reference”, but I’ve also done a fair bit of C++ coding. I find that C++ is philosophy-agnostic, but the team I worked on the most had a “prefer zero-copy” philosophy. I unconsciously brought this with me to Rust and have been treating clone() as a code smell. But the consensus on this thread seems to be that &'a on a struct is smellier. Is that about right?


#7

I don’t think they are a code smell. Just be aware that a struct with a lifetime parameter tends to act very different from one without, because the validity of its existence is tied to some scope, it will be very picky about what you pass it into and out of. They make heavy demands on how you structure your program.


#8

That might be a bit strong; I’d perhaps instead say that it shouldn’t be the default. I’ve seen far more people get into trouble here trying to store references than I have people with over-cloning problems. (Probably because .clone() helps make it visible if one is doing it clearly too much.)

Also, I tend to have better luck with structs holding an owned generic T that just happens to be instantiated as a reference in some cases – conveniently that means no lifetimes on the struct itself, though of course that trick doesn’t always work.


#9

I have limited experience with C++, so take the following with a grain of salt.

The scope of what you can do with references in Rust is so much larger than what you can do with C++ references that it allows you to take the zero-copy mantra to ridiculous extremes. This may explain why "don’t be afraid to .clone()" is pretty good advice.

C++ references are basically limited to what you can do with Rust’s references without explicit lifetimes. You can’t put one in a class/struct, because it’s not a “proper” type. You can write a function like int& larger(int& a, int& b), but when you start nesting functions like that and the call tree gets complicated, it becomes impossible to keep track of all the lifetimes in your head, and the compiler only sometimes gives moderately useful advice. There are reference_wrapper and string_view (rough approximations to Rust’s &T and &str), but they’re cumbersome to use and without explicit lifetimes there’s always a decent chance you’re using them wrong. That chance gets bigger the more reference_wrappers you have. And a lot of people don’t use them, anyway, due to unfamiliarity, being stuck with an older version of the language, or in order to avoid the limitations I just mentioned.

Compare this to Rust. You can stuff a reference in an iterator in a struct and send it up the call stack, over to another thread, and halfway across the galaxy, and if the compiler lets you, you know it’s fine! You can do everything zero-copy, even stuff you wouldn’t do in C++ because it would make reasoning about lifetimes difficult.

Except… you still have to reason about all those lifetimes, because the compiler won’t let you ignore them. Things that you have good reason to believe are safe may be difficult or impossible to prove safe. And lifetimes are kind of infectious; you can’t usually just add a reference in one place and be done because the whole API has to be updated.

So how do you avoid that? Basically stick with the rules for C++ references: don’t put them in structs and keep relationships (lifetime annotations) simple. When you do really want a reference_wrapper equivalent, you can do it safely. And rest easy knowing that the compiler has your back! :sunglasses:


#10

IMO, I think you should prefer the same philosophy in Rust as well - it doesn’t really change this at a high level. What it does do is force you to get the lifetimes right, but if you’re writing correct/memory safe C++ code, then you’re presumably already doing the mental gymnastics of ensuring memory soundness. Since lifetimes are a first class citizen in Rust, your code will get “polluted” with them but then again, so will type-generic code.

Lifetimes/references, and Rust making them safe, are one of its strengths, and although take some getting used to, I don’t think people should be pushed away from them (which I sort of sense sometimes on this forum). What’s important is to figure out the ownership and data flow semantics, and then use references and owned values accordingly.