Why is an explicity lifetime trait bound needed in the example?

fn test_send_sync_trait<'a,T:Sync>(_:T)
where &'a T : Send,
{

}

The compiler will give an error as indicated:

The parameter type `T` may not live long enough, 
     ...so that the reference type `&'a T` does not outlive the data it points at
fn test_send_sync_trait<'a,T:Sync + 'a>(_:T)
                                  ++++

I didn't adopt the suggestion, instead, I give the trait bound in the where clause, that is

fn test_send_sync_trait<'a,T:Sync>(_:T)
where &'a T : Send + 'a,
{

}

The example can also be compiled. The suggestion emitted by the compiler can be understood that we constraint T should outlive 'a. However, I don't understand the contract in the second. Isn't &'a T: 'a a redundant constraint?

Moreover, in both ways, explicitly specified T:'a seems redundant because &'a T is sufficient to indicate that T outlives 'a.

Is the explicit constraint for T:'a necessary?

Whenever bounds and WF [1] come up, there has to be a decision about

  • What code can assume the bound holds / the types are WF
  • What code must prove the bound holds / the types are WF

That distinction isn't yet fully fleshed out around GATs and HRTBs, for example.

The main RFCs around this are RFC 1214 and RFC 2093. In general the function body can assume where clauses hold, and the caller has to prove they do.

And I believe what is going on in your example is:

  • With &'a T: 'a, the function can assume the where bound holds
  • With (just) &'a T: Send, the function can assume the where bound holds
    • But this does not imply &'a T is WF on its own

There needs to be a check that &'a T is well-formed at some point, or you could end up with an unsound program where a &'a T is created even though T: 'a does not hold. Some incidents of such unsoundness being allowed by the compiler was one of the drivers behind RFC 1214. &'a T is only sufficient to indicate T: 'a if the check is being performed.

Adding the outlives bound is sufficient to make the caller perform the check in the example.

One could imagine a tweak to the rules where a clause like &'a T: Send implied &'a T was well-formed / caused the caller to prove it was well-formed. However, I'm not sure it's backwards compatible (and haven't done the legwork to have a firm opinion either way).


  1. well-formedness, e.g. T: 'a must hold for &'a T to be well-formed ↩ī¸Ž

4 Likes

So, what is the key point in the example? Is it meaning that &'a T appeared in the signature cannot imply T : 'a, so we must explicitly specify the trait bound?

&'a T appearing somewhere in the signature (including where clauses) doesn't always imply a T: 'a bound, which is why you had to add the bound (or rather, a bound which directly or indirectly provides T: 'a). Whether or not it could always imply a T: 'a bound, I'm less sure; I'd have to experiment.

(Probably it would be less special-cased at least, like in-such-in-such position types are assumed WF with regard to lifetimes by the function body and this must be proved by the caller.)

2 Likes

So, the question is turned into that:

  1. If &'a T can always imply T: 'a, the explicit lifetime trait bound requested by the compiler is redundant,
  2. Otherwise, we must explicitly give the trait bound.

However, I cannot figure out an example that can form a reference where the behind type does not outlive the lifetime of the reference except that we use the unsafe pointer. In the unsafe pointer case, the constraint still can be bypassed even though we explicitly specify T: 'a.

What I meant is, whenever we form a type like &'a T, the compiler should assume we have an extra trait bound: T: 'a, isn't it? This is similar to

impl<'a> A<'a>{
   fn show<'b>(&'b self){}
}

The definition of the method can imply A<'a>: 'b and 'a:'b, rather than requiring us to explicitly specify like where A<'a>: 'b, 'a: 'b. Why the compiler does not assume T:'a for &'a T?

The rules are syntactical and presence of &'a T in arguments are part of the rules but the presence of &'a T: Trait are not.


This requires the bounds, like a reference does.

#[derive(Clone, Copy)]
struct NonRef<'a, T: 'a>(&'a T);

fn foo<'a, T>() where NonRef<'a, T>: Copy {}

This requires the T: Copy bound too.

struct Bar<T: Copy>(T);

fn foo<T>() where Bar<T>: {}

This RFC would make the latter bounds implied, and probably the former too (still haven't done the legwork). But there are negative consequences too, so I'm not convinced it's a net benefit. Maybe there are less or no negative consequences for lifetimes specifically, but I'm not confident about that (still haven't done the legwork).

There's a lot of details in the implied bounds I haven't deep-dived on; perhaps they lend insight on why this isn't a slam-dunk to implement either. (As far as I know it's still waiting on the next-generation trait solver.)

3 Likes

That's the point :slight_smile: If you could achieve this safely, that would mean the language and/or the compiler is broken.

Does that mean we have room for improvement in these aspects? Such as the appearance &'a T: Trait can extra imply T:'a and &'a T : 'a.

If you could achieve this safely, that would mean the language and/or the compiler is broken.

So, this is what I think, &'a T: Trait can be sufficient to imply T: 'a and &'a T: 'a instead of requiring us to explicitly specify the lifetime trait bounds because &'a T(i.e. as a valid reference) always can be assumed to that the behind type T must outlive 'a.

I'm undecided if it's an improvement or not (because I haven't played around or thought about if it has downsides like implied trait bounds do), and I haven't deep-dived on anything to confirm it's not a breaking change on its own, but my intuition is that such a change is viable.

Might not be the easiest thing to implement; I only skimmed the technical bits, but the implied bounds RFC does have a lot of technical bits which are summarized as "blocked on the next-gen trait solver". That said, there has been recent movement on that front.

2 Likes

I don't follow this. There is a distinction with WF-ness that continues to elude me. When you write x: &'a T as a function parameter the only way you could ever call the function is if the type is inhabited which would imply T: 'a. But writing something like &'a T: Trait is different; it could be vacuously true if &'a T is uninhabited. (I don't think this is the way the experts think about types, but it makes sense to me.)

1 Like

I'm not sure about the difference here. When you write &'a T in a function where clause in the function signature, the type &'a T must be a valid type, otherwise, the whole function definition does not make sense. Since &'a T is a valid type, the T must outlive 'a anyway.

This gets to what WF means. Are you allowed to write down a non-WF type and it just acts like ! (Never), since you can't construct a value of the type? Or are you not allowed to write one down in the first place, and Rust adds bounds to make all types WF?

The implied bound process is fraught because you can't make bounds explicit in HRTBs and the Rust compiler makes some illegal deductions. I asked about this on IRLO eight months ago, because I was confused back then and still am now.There's a soundness hole in the Rust type system that has been open for eight years due to this, which seems bad.

1 Like