Are lifetimes ignored for ZST structs?

I tried to simplify this code but couldn't. Anyways, here is it:

use std::marker::PhantomData;

#[derive(Default)]
struct Outer<'a> {
    phantom: PhantomData<&'a ()>,
}

impl<'a> Outer<'a> {
    fn run(&'a mut self, f: impl FnOnce(Inner<'a>)) {
        let inner = Inner {
            outer: self, // COMMENT THIS OUT
            phantom: Default::default(),
        };
        f(inner);
    }
}

struct Inner<'a> {
    outer: &'a mut Outer<'a>, // COMMENT THIS OUT
    phantom: PhantomData<&'a ()>,
}

impl<'a> Inner<'a> {
    fn run(self, f: impl FnOnce() -> usize + 'a) {
        f();
    }
}

fn main() {
    Outer::default().run(|inner| {
        let value = 10;
        inner.run(|| value) // error[E0373]: closure may outlive the current function, but it borrows `value`, which is owned by the current function
    });
}

Playground (comiples): Rust Playground
Playground (error): Rust Playground

This code doesn't compile an it shouldn't. But... If I comment out two lines above it will compile.
This seams strange because I didn't change "public" definition of anything visible in main, only internal implementation.

Why does it compile if lifetimes should explicitly forbit it?

Are lifetimes ignored for ZST structs?

No. It does not care about size.

This seams strange because I didn't change "public" definition of anything visible in main, only internal implementation.

Lifetime parameter has variance on it, and the variance is inferred from the struct's member. So, yes, you changed something visible to outside. Just like adding a non-Send field will make the whole struct non-Send.

3 Likes

It's because you are changing the variance of Inner. In the version that compiles, it will take the Inner<'long> and convert it into Inner<'short>, with 'short being short enough that value can be borrowed for 'short.

With the mutable reference, Inner becomes invariant, so Inner<'long> is no longer convertible into Inner<'short>.

This is also why it compiles with an &'a Outer<'a>.

1 Like

Note that (also because of invariance) it is a bad idea to create &'a mut Outer<'a>, regardless of whether you store it in another struct or not, and &'a mut self is that. You've asking for a mutable borrow of the Outer that lasts as long as the contents of Outer do; therefore the borrow will be perpetual, and it will be impossible to use Outer more than exactly once.

When you use &mut references, because they are invariant in their referent type, you need to keep the lifetime in their referent distinct from the lifetime of the reference; don't try to use a single lifetime parameter for everything. That is: &'b mut Outer<'a>, not &'a mut Outer<'a>.

4 Likes

@zirconium-n, @alice thanks, nice catch. rust just finds new ways to confuse me with each new day.

BTW maybe you know why this was not made more explicit? I understand regarding Send/Non-Send as it can propagate through all tree of structs but for lifecycles it looks a little weird.

What do you think needs to be more explicit? The current behavior is the only correct one, so it's not like the user has a choice. Not propagating variance would basically make all the code using types that wrap mutable references unsound.

Variance propagates too. Also,

Why variance should be inferred

The main reason we chose inference over declarations is that variance is rather tricky business. Most of the time, it's annoying to have to think about it, since it's a purely mechanical thing. The main reason that it pops up from time to time in Rust today (specifically, in examples like the one above) is because we ignore the results of inference and just make everything invariant.

But in fact there is another reason to prefer inference. When manually specifying variance, it is easy to get those manual specifications wrong. There is one example later on where the author did this, but using the mechanisms described in this RFC to guide the inference actually led to the correct solution.

That said, I've wished for a better tool than PhantomData from time to time.

2 Likes