Here is a simplified version of what I want to archive:
struct Foo<\'a> {
boo: Option<&\'a mut String>,
}
fn main() {
let mut foo =
This is obviously completely safe
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:
#![feature(nll)]
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 only because foo
is never used once it would be invalid (after string
goes out of scope), not because you set the value to None
. Trying to print out the value after the innermost scope would still result in an error.
Is it possible to have a struct which contains a reference to a value which has a shorter lifetime than the struct?
The purpose of Rust's borrowing system is to ensure that things holding references do not live longer than the referred-to item.
Maybe, so long as you don't make use of the reference after it is no longer valid. This works, for example:
#![feature(nll)]
struct Foo<'a> {
boo: Option<&'a mut String>,
}
fn main() {
let mut foo = Foo { boo: None };
// This lives less than `foo`
let mut string1 = "Hello".to_string();
foo.boo = Some(&mut string1);
// This lives less than both `foo` and `string1`!
let mut string2 = "Goodbye".to_string();
foo.boo = Some(&mut string2);
}
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 where string
is alive. However, the lifetime of foo
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:
struct FooCore {
size: i32,
}
struct Foo<'a> {
core: FooCore,
boo: &'a mut String,
}
fn main() {
let core = FooCore { size: 42 };
let core = {
let mut string = "Hello".to_string();
let foo = Foo { core, boo: &mut string };
foo.boo.push_str(", I am foo!");
foo.core
}; // string goes out of scope. foo does not reference string anymore
} // foo goes out of scope
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:
struct Foo<'a> {
boo: Option<&'a mut String>,
}
impl<'a> Foo<'a> {
fn set<'b>(self, boo: &'b mut String) -> Foo<'b> {
Foo { boo: Some(boo) }
}
fn unset(self) -> Foo<'static> {
Foo { boo: None }
}
}
fn main() {
let foo = Foo { boo: None };
let foo = {
let mut string = "Hello".to_string();
let mut foo = foo.set(&mut string);
foo.boo.as_mut().unwrap().push_str(", I am foo!");
foo.unset()
}; // string goes out of scope. foo does not reference string anymore
} // foo goes out of scope
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 of RefCell
to imitate the code in the question.
use std::rc::{Rc,Weak};
use std::cell::RefCell;
struct Foo {
boo: Weak<RefCell<String>>,
}
fn main() {
let mut foo = Foo { boo: Weak::new() };
{
// create a string with a shorter lifetime than foo
let string = "Hello".to_string();
// move the string behind an Rc pointer
let rc1 = Rc::new(RefCell::new(string));
// weaken the pointer to store it in foo
foo.boo = Rc::downgrade(&rc1);
// accessing the string
let rc2 = foo.boo.upgrade().unwrap();
assert_eq!("Hello", *rc2.borrow());
// mutating the string
let rc3 = foo.boo.upgrade().unwrap();
rc3.borrow_mut().push_str(", I am foo!");
assert_eq!("Hello, I am foo!", *rc3.borrow());
} // rc1, rc2 and rc3 go out of scope and string is automatically dropped.
// foo.boo now refers to a dropped value and cannot be upgraded anymore.
assert!(foo.boo.upgrade().is_none());
}
Notice that I didn't have to reassign foo.boo
before string
went out of scope, like in your example -- the Weak
pointer is automatically marked invalid when the last extant Rc
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.