Non-lexical lifetimes (NLL) of mutable references to Vec<T>

According to one of the responses from @steveklabnik in this post, “things that implement Drop go out of scope at the end of the lexical scope they’re defined in”. If this is true, then why can I have multiple mutable references to a Vec<u32> instance? For example, rustc 1.34.1 compiles the following code:

#![allow(unused)]
fn main() -> () {
    let mut x: Vec<u32> = Vec::new();

    let y = &mut x;

    let z = &mut x;
}

From what I’ve read, Vec<T> implements the Drop trait; so I am a little confused why this compiles. Admittedly I just finished Chapter 4 of The Rust Programming Language, so I probably shouldn’t be “jumping around” like this.

Vec<T> implements Drop, that’s right. But the thing going out of scope is not the vector, but reference to it, &mut Vec<T>, and the reference doesn’t (and shouldn’t) implement Drop.

3 Likes

Here is an example that shows the difference between Drop and NLL’s end of borrows:

fn main ()
{
    let mut x: i32 = 42;
    let mut v: Vec<&mut i32> = vec![&mut x]; // &mut borrow -----+
    v   .iter_mut()                                 //           |
        .for_each(|at_at_x| **at_at_x += 27)        //           |
    ; // <--- last use of v items -------------------------------+
    let at_x: &i32 = &x; //                                      |
    println!("{}", *at_x); //                                    |
} // <------- v is dropped here ---------------------------------+

Quoting myself from another thread:

So, in this case, the vector v (made of pointers) is still dropped at the end of main()'s scope, but since Vec's Drop implementation does not access its items, they are allowed to dangle (unsafe and unstable #[may_dangle] attribute) at Drop time; thus the as soon as it is no longer used point is not reached at the end of the scope but in the middle of it, at the line with a semicolon. That’s why having and using other borrows of x is allowed in between these two points.

2 Likes

This was very useful. I apologize for my ignorance, but how would a dangling reference exist even if Vec<T> accessed its items in its drop function? The drop function in the code you wrote in the linked thread appears to access its data, but it runs perfectly fine.

Drop runs at the end of the same lexical scope as the value being dropped. So it can safely access the value. After a value is dropped it’s memory is freed.

See this playground

Code snippet
#![feature(dropck_eyepatch)]
#![allow(unused)]
//! Examples to better understand
//! How Rust borrow checker and drop checker interact

macro_rules! not {($condition:expr) => (
    !$condition
)}

macro_rules! has_drop_glue {($value:expr) => ({
    fn needs_drop_of_val<T> (_: &'_ T) -> bool {
        ::core::mem::needs_drop::<T>()
    }

    needs_drop_of_val(&$value)
})}

/// Vec-based example.
/// Can you see why it could be problematic?
/// We will explore it with the next examples.
mod vec_example {
    fn main ()
    {
        let mut v = Vec::with_capacity(1);
        assert!(
            has_drop_glue!(v) // v has drop glue (i.e. executes code on drop)
        );
        {
            let x = 42;
            let at_x = &x;
            v.push(at_x);
        } // x is dropped here
    }    // v is dropped here
}

mod example_1 {
    struct VecOfOne<T> /* = */ (
        Option<T>
    );
    
    impl<T> VecOfOne<T> {
        fn new () -> Self
        {
            Self(None)
        }
    
        fn push (&mut self, value: T)
        {
            self.0 = Some(value);
        }
    }
    
    fn main ()
    {
        let mut v = VecOfOne::new();
        assert!(
            not!(has_drop_glue!(v)) // no code executed on drop !
        );
        {
            let x = 42;
            let at_x = &x;
            v.push(at_x);
        } // x is dropped here
    }    // while at_x dangles,
         // v is dropped but no code is executed
         // => fine
}


mod example_2 {
    struct VecOfOne<T> /* = */ (
        Option<T>
    );
    
    impl<T> VecOfOne<T> {
        fn new () -> Self
        {
            Self(None)
        }
    
        fn push (&mut self, value: T)
        {
            self.0 = Some(value);
        }
    }
    
    impl<T> Drop for VecOfOne<T> {
        /// Although self.0 is not dereferenced, Rust does not know it
        /// so it is implicitly required that Self not have any dangling references
        /// Thus T cannot be dangling
        fn drop (&mut self) {}
    }
    
    fn main ()
    {
        let mut v = VecOfOne::new();
        assert!(
            has_drop_glue!(v) // v has drop glue (i.e. executes code on drop)
        );
        {
            let x = 42;
            let at_x = &x;
            v.push(at_x);
        } // x is dropped here
    }    // while at_x dangles,
         // v is dropped and executes code
         // => NOT FINE
}


mod example_3 {
    struct VecOfOne<T> /* = */ (
        Option<T>
    );
    
    impl<T> VecOfOne<T> {
        fn new () -> Self
        {
            Self(None)
        }
    
        fn push (&mut self, value: T)
        {
            self.0 = Some(value);
        }
    }
    
    /// We promise rust that it is safe for self.0 to be a dangling pointer,
    /// since no code in the drop glue dereferences it.
    unsafe impl<#[may_dangle] T> Drop for VecOfOne<T> {
        fn drop (&mut self) {}
    }
    
    fn main ()
    {
        let mut v = VecOfOne::new();
        assert!(
            has_drop_glue!(v) // v has drop glue (i.e. executes code on drop)
        );
        {
            let x = 42;
            let at_x = &x;
            v.push(at_x);
        } // x is dropped here
    }    // while at_x dangles,
         // v is dropped and executes code
         // we have promised Rust that the executed code does not deref at_x
         // => Fine
}
1 Like

OK, it appears that I erroneously thought that memory is deallocated (or popped off the stack) in the reverse order of variable declaration. If x can be popped off the stack before drop is called, then it certainly makes sense that a dangling reference can occur. Thank you.

I have not commented my example well enough, since your assumption was right, so please do not unlearn it :sweat_smile:

When I commented "at_x is dangling", I meant “the reference to x that remains in v is dangling”

Tip: you may be interested in the show MIR option in the Playground:

1 Like

Hm, I guess I’m still a little confused then. If x is still around, then I don’t see how the reference to x is “dangling”. I think it’s clear that I should at least complete The Rust Programming Language as well as read The Rustonomicon, particularly the lifetimes section. Sometimes I get inpatient and ask questions whose answers I won’t be able to fully understand until later down the road. I apologize for taking up too much of your time.