How NLL for struct works?

Sometimes struct follows NLL rules. For example, this code is compilable (playground). In its main function:

fn main() {
    let mut i = 3;
    let foo = Foo{r: &i, v: vec![123]};
    let bar = Bar{f: foo, v: vec![222]};
    let r2 = &mut i;
    *r2 = 5;
    println!("{}", i);
}

That *r2 = 5; is legal implies the lifetime of foo ends before it, so foo follows NLL.

But this code is erroneous (playground). In it:

fn main() {
    let mut a = Vec::new();
    let mut foo = Foo::new(|v| {
        for i in v {
            a.push(i);
        }
    });
    foo.fun(1);
    println!("{:?}", a);
}

results in a compilation error:

error[E0502]: cannot borrow `a` as immutable because it is also borrowed as mutable
  --> src/main.rs:24:22
   |
18 |     let mut foo = Foo::new(|v| {
   |                            --- mutable borrow occurs here
19 |         for i in v {
20 |             a.push(i);
   |             - first borrow occurs due to use of `a` in closure
...
24 |     println!("{:?}", a);
   |                      ^ immutable borrow occurs here
25 | }
   | - mutable borrow might be used here, when `foo` is dropped and runs the destructor for type `Foo<'_, i32>`

which means that the lifetime of foo ends after println!("{:?}", a);, showing that foo here follows lexical rules instead of NLL.

Why is the difference, and how exactly NLL for struct works?

The difference is that the second case uses a type with a destructor (Box<T>). The destructor runs when the owner goes out of scope. (Destructors always follow simple scope-based rules; they don't change based on how a value is used.) So the borrow must live for the entire scope.

2 Likes

...but you can explicitly change where it goes out of scope by using drop(foo); before the println!() (for example). The difference is maintained for the sake of backwards compatibility (pre-NLL code may have relied on destructors running when they did).

1 Like

But Vec implements Drop (which means it has destructor as well?). And in the first code example foo has a Vec field. So foo's lifetime should also be lexical?

The compiler can tell that the destructor of the vector cannot access the immutable reference, so it allows it.

1 Like

Foo and Bar don't implement Drop though -- compiler generated "drop glue", which is not a Drop implementation, recursively drops the members. I'd link you to documentation explaining this, but as far as I know, there is no official documentation which covers all of these details.

(If you impl Drop for your structs, you will get a similar error.)

This covers what drop glue does, but I don't think it covers the difference in NLL or lexical terms.

Here's a previous forum thread on the topic as well.

1 Like