Can the lifetime parameter in dyn trait object not be covariance?

Please consider this example

use std::{marker::PhantomData};
trait DoSomething<T> {
	fn do_sth(&self, value: T);
}
impl<'a, T> DoSomething<T> for &'a usize {
	fn do_sth(&self, value: T) {
	}
}
fn foo<'a>(b: &dyn DoSomething<&'a usize>) {
	let s: usize = 10;
	b.do_sth(&s) // #1 error[E0597]: `s` does not live long enough
}

struct DoSomething2<T>{
	v:PhantomData<T>
}

impl<T> DoSomething2<T>{
	fn do_sth(&self, v:T){}
}

fn foo2<'a>(b:&DoSomething2<&'a usize>){
	let s = 10;
	b.do_sth(&s); // #2
}
fn main() {

}

#1 is an error but #2 is ok. What's the reason here? It sounds like the covariance of the lifetime parameter behind the trait object is restricted.

IMO, in the second case, the lifetime 'a can be shrunk to the innermost block of foo2 in order to make the lifetime of the reference of s can satisfy the 'a. However, the first case is a bit different.

I think this is similar to an issue I ran into a couple weeks ago. @quinedot had a good example of how interior mutability can be used behind a trait object to cause problems, though this example is a little different than mine was [1]

Essentially, you could create a type with interior mutability that implemented the trait and captured the reference passed to that method. If the borrow checker allowed your code to work, that captured reference could be pointing to a completely different variable's storage when it accessed it later.


  1. in fact a reference to a trait object worked just fine in my case, it was only an owning trait object where the problems started ↩︎

I view your question from the link. It seems there are no formal answers to that question. This part might be a bit obscure.

Yeah as far as I know there's no way to relax the variance of the trait object type parameters.

Conceptually I don't think there's any reason it would be too hard to add a way to do that to the language though.

Is that to say, the lifetime in a trait object is invariant? In my example, it embodies that the s defined in the function's body should outlive 'a, which would be over its maximum scope.

If I remember correctly, trait type parameters actually had variance, and it was removed.

1 Like

Ooo that's interesting, I keep meaning to dig into this more and then Not Doing It.

I'll have to see if I can dig up anything about that, thank you!

See this PR https://github.com/rust-lang/rust/pull/23938

1 Like

I think that's consistent with what was discussed in that other thread, yes.

While variance together with trait objects is a bit weird, I don't think this is the case here.

In #1 you have a type that implements DoSomething<&'a usize> and you know it's valid for that 'a only. Someone might implement DoSomething<&'static usize> for their type and pass an instance of that type to your foo function, in that case calling do_sth with &s would not be valid.
Note that even if variance worked with trait objects this would still be wrong! That's because DoSomething<T> at best can be contravariant in T since it reads it.

#2 instead works because the compiler statically knows that DoSomething2 is covariant.

ps: the impl<'a, T> DoSomething<T> for &'a usize is useless in this example, maybe that's confusing you?

2 Likes

Which part in rust books says about this?

You just break the lifetime rule here. The &'a usize means an outer borrow, and you actually pass it an inner borrow, like this:

fn f<'s>(s: &'s str) {
    let _: &'s str = s;
    let ss = String::new();
    let _: &'s str = &ss; // `ss` does not live long enough
}

Edit: I delete my covariance analysis.

The issue is why the second case is ok? which has almost the same form as the first.

The book says nothing because traits currently don't have variance. The fact that DoSomething<T> (the trait) can at best be contravariant in T is just a logical fact. If it could be covariant it would result in unsoundness with just safe code (like passing a non-'static lifetime where a 'static lifetime was expected).

My previous covariance analysis is wrong, and I deleted it.

So we can make the first case pass:

fn foo(b: &(dyn for <'a> DoSomething<&'a usize>)) {
	let s: usize = 10;
	b.do_sth(&s) 
}

Because I just find the description of subtyping in the reference:

Higher-ranked function pointers and trait objects have another subtype relation. They are subtypes of types that are given by substitutions of the higher-ranked lifetimes. Some examples: ...

The second case is failed if we make it invariant over T:

struct DoSomething2<T> {
    v: PhantomData<*mut T>,
}

Yes, using the HRTB can let the example work, however, I don't know why it can.

If b can DoSomething for any lifetime, no matter how short (or long) -- that's what the HRTB means -- than it can DoSomething for the local borrow in foo.

1 Like

The simple way to understand the effect of HRTB is, it can make the lifetime parameter accept any lifetime, Right?

I don't know that I really agree with that wording, it makes it sound like a HRTB makes a function strictly more expressive. It's more flexible for the function body but more restrictive for callers of the function.

A trait bound (HR or not) is part of a contract:

  • Callers must meet the bound or they can't call the function
  • Inside the body of the function, you can assume the bound was met
    • In this case it makes it possible to utilize the trait with a local borrow (which is a typical reason to need a HRTB: you need something to work with a lifetime shorter than any caller can name)

So while it's true in this case that means the "lifetime parameter accept[s] any lifetime", it also made it so you can't call the function with something that doesn't work for every lifetime.

Playground.

1 Like

I think it is beyond my knowledge of rust. This part is obscure to me. Especially for the case bar(&&local_unit);. Since the borrow of the block variable i within the function body can satisfy the lifetime declared by the HRTB, why does the lifetime of the &local_unit whose lifetime definitely lives longer than the borrow of the local variable not satisfy the bound?