Here is a simplified version of what I want to archive:
struct Foo<'a> {
boo: Option<&'a mut String>,
}
fn main() {
let mut foo = Foo { boo: None };
{
let mut string = "Hello".to_string();
foo.boo = Some(&mut string);
foo.boo.unwrap().push_str(", I am foo!");
foo.boo = None;
} // string goes out of scope. foo does not reference string anymore
} // foo goes out of scope
This is obviously completely safe as foo.boo
is None
once string
goes out of scope.
Is there a way to tell this to the compiler?
What is obvious to humans isn't always obvious to the compiler; sometimes the compiler isn't as smart as humans (but it's way more vigilant!).
In this case, your original code compiles when non-lexical lifetimes are enabled:
This is only because
foo
is never used once it would be invalid (afterstring
goes out of scope), not because you set the value toNone
. Trying to print out the value after the innermost scope would still result in an error.The purpose of Rust's borrowing system is to ensure that things holding references do not live longer than the referred-to item.
After non-lexical lifetimes
Maybe, so long as you don't make use of the reference after it is no longer valid. This works, for example:
Before non-lexical lifetimes
No. The borrow checker is not smart enough to tell that you cannot / don't use the reference after it would be invalid. It's overly conservative.
In this case, you are running into the fact that lifetimes are represented as part of the type. Said another way, the generic lifetime parameter
'a
has been "filled in" with a concrete lifetime value covering the lines wherestring
is alive. However, the lifetime offoo
is longer than those lines, thus you get an error.The compiler does not look at what actions your code takes; once it has seen that you parameterize it with that specific lifetime, that's what it is.
The usual fix I would reach for is to split the type into two parts, those that need the reference and those that don't:
Note how this removes the need for the
Option
— your types now tell you if the string is present or not.An alternate solution would be to map the whole type when setting the string. In this case, we consume the whole variable and change the type by changing the lifetime:
Shepmaster's answer is completely correct: you can't express this with lifetimes, which are a compile time feature. But if you're trying to replicate something that would work in a managed language, you can use reference counting to enforce safety at run time.
(Safety in the usual Rust sense of memory safety. Panics and leaks are still possible in safe Rust; there are good reasons for this, but that's a topic for another question.)
Here's an example (playground).
Rc
pointers disallow mutation, so I had to add a layer ofRefCell
to imitate the code in the question.Notice that I didn't have to reassign
foo.boo
beforestring
went out of scope, like in your example -- theWeak
pointer is automatically marked invalid when the last extantRc
pointer is dropped. This is one way in which Rust's type system still helps you enforce memory safety even after dropping the strong compile-time guarantees of shared&
pointers.