Why does `&'a T` in a struct require `T: 'a`?

intuitively, it makes sense, a reference can't "outlive" what it's referencing.

but i'm having a hard time coming up with an example, where, if a struct didn't have the T: 'a bound, other analyses would miss UB.

say we have

struct R<'a, 'b>(&'a mut &'b mut i32);

let foo1;
let foo2;
let bar;
{
    let mut v = 42;
    foo1 = &mut v;
    foo2 = R(&mut foo1);
    bar = &foo2;
}
bar;

then the use of bar makes the regions in R live, which make the loans &mut v and &mut foo1 live.

this example obviously doesn't disprove that T: 'a is necessary in structs.
but maybe it helps to make my confused thought process a little clearer.
i can't figure out which case the T: 'a bound in structs covers, that local analysis in a function doesn't capture.

ps: i accidentally created this topic a little too early by pressing cmd+enter.

It's trivial:

static FOO: Foo<'non_static> = Foo::new();

fn bar() {
    let x: &'static Foo<'non_static> = &FOO;
    x.do_stuff(); // uses a dangling pointer
}

i'm not sure i understand :thinking:

what is the dangling pointer?

statics require T: 'static, don't they? so FOO would have to be of Foo<'static>.

2 Likes

maybe i should clarify the question:

if i create a struct

struct Foo<'a, T> {
    x: &'a T,
}

the compiler adds a constraint T: 'a to the struct definition.

why is that?

what's an example of a piece of code that uses a Foo (or a type like it), and that leads to UB without the additional T: 'a bound, that was added by the compiler?

Interesting question. As far as I’m aware, lifetime bounds such as 'b: 'a, are important for expressing (function) signatures only, anyways, so examples that show why e.g. &'a mut &'b mut i32 should come with a 'b: 'a restriction can only become apparent once some abstraction layer / interface via function signatures is involved.

That being said, the fact that every mention of the type &'a T requires 'a: T immediately is likely not necessary anyways, but perhaps serves for convenience, so that this bound can even become automatically implied. However, without &'a T as a type requiring 'a: T for “well-formedness”, at least some operations involving &'a T (or &'a mut T) ought to require the outlives-relation in question for soundness.

Let’s see… thinking about this for a while, it seems harder to come up with all that many examples, unless I’m missing something obvious. One example however would be turning &'a T with T: Trait into &'a dyn Trait. This conversion requires T: 'a, as &'a dyn Trait is syntactic sugar for &'a (dyn Trait + 'a).

Even if the use-cases that actually need the bounds are not super common, the argument the other way is that &'a T without 'a: T isn’t all that useful of a type in the first place. This could serve as an argument in favor of why we want the implied bounds T: 'a from such types, because in cases where you do need the bound, it’s useful you didn’t have to think about adding them (and potentially thus transitively adding them to a bunch of places that call your place that now needs the T: 'a, too). And implied bounds work well with well-formedness requirements. If the type &'a mut &'b mut T without 'b: 'a was ever allowed, you’d want a way to opt out of the implied bound[1], otherwise, adding complexity for relatively unnecessary cases.


Note that it’s a different question whether to allow &'a mut &'b mut T values where the lifetime 'b is dead, instead of simply having two live lifetime where 'b doesn’t outlive 'a. If the former is disallowed, then my argument above holds that even ever accounting for the case that 'b doesn’t outlive 'a is quite use-less, because what does that even mean besides that 'b could become dead while the reference still exists and is live?

Handling references that are dead is not really supported in Rust at all, particularly across API-boundaries[2] (e.g. a local variable can actually hold dead references and implicit drops can handle them, too, but that’s about it. And (live) references pointing to dead references seems to me like a way of handling dead references. It seems potentially interesting to think about whether that’s something that can be changed, too, and if it is, perhaps &'a mut &'b mut i32 without a 'b: 'a bound could (subsequently) become more interesting, too.


  1. though to be fair, it does seem worth thinking about this more… maybe there can be value in not requiring the bounds for well-formedness, but still having them implied? I'm thinking of compiler errors from… say… adding some Q: AsRef<&'a T> bound without anything in your signature implying T: 'a. Maybe enforce this only indirectly for the caller, not force the definition to immediately call out T: 'a for this :thinking: ↩︎

  2. because a lifetime parameter of a function is always considered live at least for the full duration of the function call ↩︎

1 Like

Following along with this and the example I gave (i.e. creating trait objects), what the rules of well-formedness and implied bounds currently allow you for your example struct is to write a function

use std::fmt;

#[derive(Debug)]
struct Foo<'a, T> {
    x: &'a T,
}

fn f<'a>(x: &'a Foo<&str>) -> &'a dyn fmt::Debug { x }

without having to introduce a second named lifetime for x: &'a Foo<&'b str> and manually writing 'b: 'a, which is necessary for soundness so the &'a dyn fmt::Debug trait object, which no longer mentions the other lifetime, can soundly dereference the &str when you print it.

i had also considered that. a reference &'a T without T: 'a doesn't seem particularly useful.

on the other hand, with the implicit bound, any reference in T can be returned as &'a U, which seems useful.

That's exactly the point and what you were asking about. If &'a T didn't require T: 'a, then you would be able to create a long-living reference to something that's only valid for a shorter time, making the reference dangling.

what's an example of a piece of code that uses a Foo (or a type like it), and that leads to UB without the additional T: 'a bound, that was added by the compiler?

That's precisely what my code demonstrated.

I’m, too, having a hard time following what your code demonstrates at all, since 'non_static is not a defined lifetime specifier, so it’s impossible to define a static variable like this, and it’s impossible to know when you consider 'non_static to be valid and when not (apparently not in the body of bar()?). One could of course argue about creating a &'static Foo<'non_static> using Box::leak in a function fn bar<'non_static>(), but then 'non_static isn’t dangling inside of bar.

It's an example. Foo is not a concrete or existing type, either, yet nobody raises an eyebrow about it.

The point is that the lifetime is not 'static, which implies that it's shorter than 'static. That's enough to make the code unsound (if it compiled in the first place), because you can access something that's invalid in the accessing scope.

The code would equally demonstrate why this would be wrong were it allowed with the longer lifetime not being 'static. However, that would have not had any added information compared to OP's post itself, since s/he was confused precisely about the abstract case.

then you would be able to create a long-living reference to something that's only valid for a shorter time

if we ignore structs, for a moment, just using references inside a function, we can't create a long-living reference to a short lived value, because the (re-)borrowing & variance constraints make sure, anything that a reference may point to is required to be valid, whenever the reference is live.
in other words, the local analysis of references in functions is sound.

the question is now, when we hide a reference inside a struct, what's an example, where the local analysis is no longer sufficient without the added T: 'a bound.
since the references still need to be created inside a function, the borrow checker still knows about their regions, which then become generic args to the struct, when we "hide" a reference inside a struct. and (afaik), whenever the struct is live, the generic regions are live, so it doesn't seem like any information is getting lost.
it seems, if the lack of the added T: 'a bound could lead to UB, it would have to be in a function that uses the struct, and perhaps returns a reference out of it, which at the callsite now no longer requires the correct loans.

Foo can be defined. Non-'static lifetimes accessible in global scope cannot be defined (and static variable definitions cannot access local type/lifetime/const params either).

Yes, but those are all orthogonal to the issue being demonstrated. Just because this specific example doesn't work for some other, irrelevant reasons too doesn't mean that the incorrect lifetime annotations should be allowed, The argument that some other compilation pass will catch it anyway is not really valid since it would require different compilation passes to all know about each other's special cases. It would be disastrous.

I'm feeling like you are splitting hair for no good reason. I'm pretty sure you understand the issue.

okay, i'm picking up no two themes here:

  1. by requiring T: 'a, for a concrete T, a function taking &'a T can return any reference inside T under the lifetime 'a without adding additional lifetimes/bounds.

  2. not T: 'a is invalid, therefore any number of passes can reject it, which may help with error messages and robustness (and avoiding disaster).

but it doesn't necessarily seem like removing the automatic T: 'a bound from struct definitions would enable users to write unsound safe code, without some other validation mechanism picking that up. which my question was about. however, given 1 & 2 above, i can see why it makes sense to require T: 'a.

I'm pretty confused about this too. Without the bound T: 'a is it ever possible to create a &'a T? When you compile a function with an argument of type &'a T you can assume you have such a value (and thus that T: 'a). But this is like proving "P implies Q." You can assume P in the body of the proof but you cannot conclude that P is true. In fact this analogy can be made more precise. I guess you could get into trouble with something like loop {} which can have any type you like. But can you write an example that terminates?

I find implied bounds confusing. The type for<'a> &'a T should probably be for<'a where T: 'a> &'a T.

1 Like

Compare this to how declaring functions work: You declare some generic parameters and have to include bounds (implicit or explicit) that guarantee the types of your inputs are well-formed and provide the functionality you need.[1] Then within the function body you[2] can assume everything is well-formed and the bounds are met. However, if you try to do something outside of those bounds, you get an error. The burden of proving the bounds falls on the call sites.[3]

Type declarations work similarly. In that case, the fields take the place of the function body; the types of the fields aren't considered inputs to the declaration. So if you want to define a &'a T field, the burden of proving &'a T is well-formed falls on the type declaration. Thus the need for the bound.[4] As a consequence, all other code can assume that if the bounds on the type constructor are met, the type is well-formed.

If it didn't work this way, the burden of proof would have to move somewhere else -- every place the type was mentioned or constructed or something like that.[5] Some part of the code has to be responsible for checking well-formedness or your compiler is unsound.[6]

You can read more in the motivation section of RFC 2093 (including the background subsection) and the other RFCs it links to.


  1. There are some exceptions around HRTB checks which are deferred to the call site. ↩︎

  2. and the compiler! ↩︎

  3. Or otherwise constructing the function item type, e.g. to create a function pointer or instance of the function item type. ↩︎

  4. Due to RFC 2093, linked below, the bound is inferred and need not be explicitly stated. ↩︎

  5. Apparently it worked this way pre-1.0. ↩︎

  6. And incidentally, the more things you don't have to explicitly declare on your type declaration (variance, auto traits...), the easier it is to accidentally introduce a breaking change. ↩︎

5 Likes

very interesting, thanks!

since you mentioned well-formedness, would it be accurate to say that the type former for references requires the T: 'a bound? ie: struct Ref<'a, T> where T: 'a (/*built-in*/);
and therefore using &'a T in a struct requires the bound, just like when using any other struct with where bounds.

Yes, &'a T and &'a mut T have the T: 'a bound. Moreover it acts like an explicit bound and not an inferred bound.

i see!
i wanna ask, why that bound is required? cause given my understanding of the NLL RFC, the local analysis always implies that bound.
but i have a feeling that the reason the NLL RFC implies that bound is precisely because that's what it's meant to do. would that be accurate?

in other words, if the type former didn't require T: 'a, the NLL rules would be more relaxed, but not sound.
for example, reborrowing adds 'b: 'a constraints for supporting prefixes, and the reason it does that is to ensure T: 'a for the special case of references to references? edit: uh, no, the 'b: 'a in reborrowing is to enforce the stack discipline.