5

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?

lncr
  • 826
  • 8
  • 16
  • If you're trying to replicate something that would work in a managed language, reference counting and [weak (non-owning) references](https://doc.rust-lang.org/std/rc/struct.Weak.html) can allow you to emulate something like this. But the safety will then be enforced at runtime, not by the compiler. – trent Aug 16 '17 at 14:00
  • What is the purpose of defining `foo` ahead of time, here? – Matthieu M. Aug 16 '17 at 14:44
  • @Matthieu M. There are other struct fields and I do this in a loop where performance is important. In this example it's completely useless :D – lncr Aug 16 '17 at 16:26
  • @trentcl While this is irrelevant in my case as the only reason I'm actually not just creating a new struct is for performance reasons, this still seems interesting. Would you mind to post an answer using either ´Rc<>` or weak references? If you don't I will just look a bit into this and create a self answer once I have the time. – lncr Aug 16 '17 at 16:32

2 Answers2

6

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
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • 3
    Note: It might be better to pass `FooCore` by reference if it's anything not trivial. – Matthieu M. Aug 16 '17 at 17:08
  • I can't get this to work if FooCore is referenced as mutable. – Cthutu May 30 '18 at 14:53
  • @Cthutu [works fine for me](https://play.rust-lang.org/?gist=ac7b54c5f8eb44437ab130d2c88ca1a2&version=stable&mode=debug). Notice how you didn't specify what you mean by "can't get this to work" even means? That means that I can't point to an appropriate Q&A that explains what problem you are experiencing. Even in comments, it's important to practice the effective communication skills that are so valuable here on SO as well as in the real world. – Shepmaster May 30 '18 at 14:59
  • It's hard to describe it because I really don't have a clue what Rust is doing. In the example above, change `core: FooCore` to `core: &mut FooCore` and you will see what I mean. – Cthutu May 30 '18 at 15:14
  • 1
    @Cthutu [I did change `core` to a mutable reference and it worked for me](https://play.rust-lang.org/?gist=ac7b54c5f8eb44437ab130d2c88ca1a2&version=stable&mode=debug), as I linked in my previous comment. *It's hard to describe it because I really don't have a clue what Rust is doing.* — I'm not asking you to describe anything. All you have to do is provide a link to the playground with the failing code. At the very least, you could copy and paste the error message you get. Rust has *really good* error messages. – Shepmaster May 30 '18 at 15:19
  • Sorry I didn't see your message until now. I've created an example code that splits up Foo into FooCore & Foo: https://gist.github.com/rust-play/e68586753b785c02b6c27c02bf90f5b0. The issue is making the FooCore instance mutable. I am not sure how to fix this yet. – Cthutu Jun 22 '18 at 12:50
  • @Cthutu no, the issue is that you are attempting to have mutable aliasing. Go back and re-read [*The Rust Programming Language* chapter on mutable references](https://doc.rust-lang.org/book/second-edition/ch04-02-references-and-borrowing.html#mutable-references). You cannot have two mutable references to the same value. – Shepmaster Jun 22 '18 at 19:19
  • I understand why it doesn't work. And I understand the rules and why they're there. What I don't understand is what I have to do to get what I want: a way to replace an instance of a mutable data structure inside another mutable data structure. I haven't found an answer to this problem. – Cthutu Jun 23 '18 at 20:16
  • For example, how can I change the code in the gist I posted to get what I want in the main function? This information would be vital. If I can't solve this simple problem in Rust, I will not be recommending it. I was a big fan up until this point. – Cthutu Jun 23 '18 at 20:17
  • 2
    @Cthutu [Solved](https://play.rust-lang.org/?gist=e0ba85f9e74a64cd3318c08c8dd4b56e&version=stable&mode=debug). [Solved](https://gist.github.com/d2965c155b891e83b6c56f7b651957a6). [Solved](https://play.rust-lang.org/?gist=3ec722e5e415f64bcefb91514a438917&version=nightly&mode=debug). I don't get what you are trying to accomplish by threatening to not recommend Rust. If Rust isn't appropriate for your case or if it's not understandable to you, *that's ok*. Not everything is for everyone — use what is comfortable to you and what lets you do what you need to do. – Shepmaster Jun 23 '18 at 21:07
  • The solution was to add braces? So redefining foo doesn't not destroy the original ? Does this mean that only scope defined by braces define the destruction points? – Cthutu Jun 23 '18 at 22:20
  • I tried refcells before and didn't get far with them. BTW, it wasn't a threat. I need to determine the right tools and if I am fighting the Rust compiler all the time then practically it won't be useful for me or my team, even in theory it is a better language than, say, C++. – Cthutu Jun 23 '18 at 22:22
  • @Cthutu Rust policy is to fight with compiler time error instead of runtime error, thus C++ is not bad on this point too. Whatever if you expect code in rust without compile error everytime, I think you will be disappointed – Stargateur Jun 23 '18 at 23:13
2

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.

trent
  • 25,033
  • 7
  • 51
  • 90
  • Pedantically, this *still* doesn't allow you to have a reference to something with a shorter lifetime ^_^. It gives you something that can tell you if the value is kept alive by something then from that allows you to get a reference. – Shepmaster Aug 17 '17 at 16:13
  • 1
    Yep, that's a more technically correct (the best kind of correct!) way of putting it. – trent Aug 18 '17 at 19:00