Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to have a struct which contains a reference to a value which has a shorter lifetime than the struct?

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?

like image 510
lncr Avatar asked Mar 08 '23 07:03

lncr


2 Answers

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.

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:

#![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); 
}

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 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
like image 57
Shepmaster Avatar answered Mar 10 '23 21:03

Shepmaster


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.

like image 43
trent Avatar answered Mar 10 '23 20:03

trent