Lifetime of things that are moved

enum ManagedSlice<'a, T: 'a> {
    Borrowed(&'a T),
    Owned(T)
}

struct B<'a>{
    m: ManagedSlice<'a, Vec<u8>>
}

impl<'a> B<'a> {
    pub fn new() -> B<'a> {
        let m = ManagedSlice::Owned(vec![]);
        B{
            m: m
        }
    }
}

fn main() {
    let b = B::new();
}

Playground

I made this code to test if I really understand lifetimes. When I create let b = B::new() I get a B<'x> with a specific lifetime 'x. In theory this should be the lifetime of m. But as you see, m's life is very short, living only inside new.

What happens to a lifetime when something moves? Does m get the lifetime of B?

What about this modification:

enum ManagedSlice<'a, T: 'a> {
    Borrowed(&'a T),
    Owned(T)
}

struct B<'a>{
    m: ManagedSlice<'a, Vec<u8>>
}

impl<'a> B<'a> {
    pub fn new(s: &'a u8) -> B<'a> {
        let m = ManagedSlice::Owned(vec![]);
        B{
            m: m
        }
    }
}

fn main() {
    
    let b;
    {
        let n = 0;
        b = B::new(&n);
    }
}

Playground

now I'm forcing 'a to have the lifetime of n. I expected this to not compile since n has a short life and b gets the type B<lifetime of n>, but b has a lifetime greater than n.

What is happening?

It is not the lifetime of m, rather it's just that the type of m is annotated with the lifetime. The only requirement to be annotated with a lifetime is that the type must be valid within the entirety of the lifetime, and all owned types are valid everywhere, and everywhere happens to include the lifetime no matter what the lifetime is.

I guess, b is uninitialized, again, after the }, i.e. your instance of B is dropped. Try calling std::mem::drop(b) after the inner }. It'll either complain about b being uninitialized or give you a lengthy error message about incompatible lifetimes. In the case no error happens, n would have to be callable outside of its scope, but that would surprise me. :thinking:

Right, if you actually use b after n has gone out of scope in your main, this will fail to compile. However, this has nothing to do with m. It's because b is annotated with the lifetime from n, so the compiler doesn't know whether it is safe for b to exist after n goes out of scope, and to be safe it assumes the answer is no. (it is in this case, but it might not be if the new method was different)

In that case, can you re-initialize b with a new B, afterwards? Theoretically, b is still in scope, just marked as uninitialized and lifetimes are not part of the type, so b could then be re-initialized and tracked with a different lifetime than the inner scope.

No, you can't do that because every value assigned to b must be of the same type, but B<'a> and B<'b> would be two different types.

fn main() {
    
    let mut b;
    {
        let n = 0;
        b = B::new(&n);
        drop(b);
    }
    let n2 = 0;
    b = B::new(&n2);
    println!("{:?}", b);
}
error[E0597]: `n` does not live long enough
  --> src/main.rs:26:20
   |
26 |         b = B::new(&n);
   |                    ^^ borrowed value does not live long enough
27 |         drop(b);
28 |     }
   |     - `n` dropped here while still borrowed
29 |     let n2 = 0;
30 |     b = B::new(&n2);
   |     - borrow might be used here, when `b` is dropped and runs the destructor for type `B<'_>`
   |
   = note: values in a scope are dropped in the opposite order they are defined

playground

The compiler error is a bit yanky here, but what I said above is the gist of it.

Good to know. This either seems like a case where the borrow checker is too restrictive (which could be relaxed eventually) or there should be a warning for declaring b in a scope it can never be used in with the the hint to move let b into the inner scope to avoid confusion.

impl<'a> B<'a> {
    pub fn new(s: &'a u8) -> B<'a> {
        let m = ManagedSlice::Owned(vec![]);
        B{
            m: m
        }
    }
}

I don't get it.

For example, in this code, it says that I return a B annotated with lifetime 'a. Which means that B has an m with an element that lives as long as 'a. However, m is something that lives just inside new. I don't get it

It means that if B contains an m that is the ManagedSlice::Borrowed variant, it can't contain a reference to anything that lives shorter than that. Since you set it to the ManagedSlice::Owned variant, there is nothing that prevents the guarantee represented lifetime parameter from being fulfilled, because there is no borrow involved in the output (so you could have it return B<'static> if you want).

Yeah. When lifetimes are annotated on a type, all it means is that the type cannot contain anything that lives for a shorter time than that. It may contain something that lives for a longer time, such as your vector.

But in

enum ManagedSlice<'a, T: 'a> {
    Borrowed(&'a T),
    Owned(T)
}

I made it such that T must live as long as 'a. Or am I interpreting it wrong? What does T: 'a means here?

The bound that T: 'a means that the type T cannot be annotated with any lifetimes shorter than 'a, or in other words, the type T must be valid for at least the lifetime 'a. The type Vec<u8> is valid forever, so it is also valid for at least 'a no matter what 'a is.

why vould Vec<u8> be valid forever?

You could keep any Vec<u8> around for the entirety of the program (forever), so there's no bound on when it is valid. Where as if you borrowed a &[u8] from the Vec<u8>, it could only be valid until the vector was moved or destroyed.

(The fact that you could keep it around forever doesn't mean that you have to keep it around forever.)

I think your explanation is correct but it does not make sense to me. If I can keep a Vec<u8> forever, I can also keep a &[u8] to this Vec forever.

Types with lifetimes, like &'a [u8] are restricted to a specific region of the program, often a specific stack frame. Trying to return them to a frame higher up in the stack will cause a borrow checker error. Types without lifetimes, like Vec<u8>, can always be moved up to a higher stack frame; there's no static limitation on how long you can keep them around.

@mbrubeck spelled out more concretely what I was getting at with moves: You can't return both a Vec and a reference into the Vec (for example) because to return the Vec, you have to move it.

Correct, for example:

fn get_static_slice(vec: Vec<u8>) -> &'static [u8] {
    Box::leak(Box::new(vec)).as_slice()
}

The above produces a &[u8] to the vector that you can keep around forever.

The problem is that how long you can keep the &[u8] around depends not on how long you could keep the Vec<u8> around, but rather on how long you actually keep it around. So if you don't keep the Vec<u8> around forever (even though you could), then you can't keep the &[u8] around forever either.

To be a bit more precise, the &[u8] is bounded by the duration of the borrow of the Vec<u8>, not just how long the vector lives in total. So since a vector can only be moved if there are no active borrows, all &[u8] into it must be destroyed before you can move the vector.

1 Like