3

I have came across a lifetime quirk that I can't understand. I am unsure how to phrase the problem so it has been difficult finding a solution by googling.

I have a function which creates an instance of a trait, and that returned impl has a lifetime bound 'a. The function takes two parameters (not_borrowed: T, borrowed: &'a str), where not_borrowed is only expected to be available in the body of the function and borrowed has a lifetime bound of 'a meaning it could be stored in the returned trait.

In this example I just have a dummy trait implemented for ().

When I then call the function from an outer function the compiler gives an error:

error[E0597]: `val` does not live long enough
  --> src/main.rs:9:28
   |
8  | fn outer<'a>(val: String) -> impl ReturnedTrait + 'a {
   |          -- lifetime `'a` defined here
9  |     let ret = create_trait(&val, "");
   |               -------------^^^^-----
   |               |            |
   |               |            borrowed value does not live long enough
   |               argument requires that `val` is borrowed for `'a`
...
13 | }
   | - `val` dropped here while still borrowed

I would not expect this to happen because I never required that T lives for 'a.

I noticed that I can get the code to compile by doing what's on the commented line below. I'm not sure why the compiler would all of a sudden change it's mind and think it's OK just because I put the value in a box?

playground

trait ReturnedTrait {}
impl<T: ReturnedTrait + ?Sized> ReturnedTrait for Box<T> {}

impl ReturnedTrait for () {}

fn create_trait<'a, T>(not_borrowed: T, borrowed: &'a str) -> impl ReturnedTrait + 'a {}

fn outer<'a>(val: String) -> impl ReturnedTrait + 'a {
    let ret = create_trait(&val, "");
    // Uncomment this line and the code compiles
    // let ret: Box<dyn ReturnedTrait + 'a> = Box::new(ret);
    ret
}
cafce25
  • 15,907
  • 4
  • 25
  • 31
Viktor W
  • 1,139
  • 6
  • 9
  • 2
    Might be related, if not a duplicate of this: [Why is the lifetime of my returned impl Trait restrained to the lifetime of its input?](https://stackoverflow.com/questions/74531071/why-is-the-lifetime-of-my-returned-impl-trait-restrained-to-the-lifetime-of-its) – cafce25 Mar 25 '23 at 00:46
  • 2
    That looks like the answer to me and I voted to close, but I've now reopened because it doesn't readily explain why boxing it fixes the problem. :/ – kmdreko Mar 25 '23 at 00:52
  • @kmdreko Hmm, this makes me wonder if the type-checking involving converting to a trait object is able to "see through" the `impl` return type. In order to create the trait object, it has to know the _actual_ return type is `()` (to create the vtable) which obviously satisfies any lifetime requirement. So maybe this is just a case where conversion to a trait object is "privileged" in that it gets to know more about the involved types than everything else. – cdhowie Mar 25 '23 at 03:31
  • @cdhowie It wouldn't surprise me, there are other situations where the compiler can "see through" impl traits. I do hope someone knows or can figure it out though since I'd be very interested in the answer; its not intuitive and I didn't even know about the behavior linked above either. – kmdreko Mar 25 '23 at 03:51
  • This is really weird because it wants `&val` to live longer, even though that lifetime is completely unrelated to `'a`. It's not seeing through the `impl Trait`; it knows the return is `'static` because you're passing it a `&'static str`. It *should* work without the box. – drewtato Mar 25 '23 at 06:06
  • 1
    Here's a playground with a couple less distractions: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=cea044c76c06ab53a529a10cf511ac3b – drewtato Mar 25 '23 at 06:12
  • Whoa, changing it to `create_trait<'a, 'b, T>(_not_borrowed: &'b T, borrowed: &'a str)` fixes it. – drewtato Mar 25 '23 at 06:14
  • @drewtato The commented link tells us that in rust, a returned impl captures the lifetime of generics. For this reason, it should not work without the box? The question now is why does it work with the box? – Viktor W Mar 25 '23 at 09:29
  • This goes even more strange: if you remove the `+ 'a`, then [it does not compile even with boxing](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=2cba143548e343ea24eba8c99cdf66a5). I assume that when you specify that it captures the lifetime, it captures the generic parameter somewhat "less strongly", and then it passes through the `Box`? Anyway, this starts to look like a bug to me. – Chayim Friedman Mar 26 '23 at 08:49

1 Answers1

0

Here's a simplified version of your code, and a few variations, which should help to explain what's going on (Rust Playground link):

#![feature(type_alias_impl_trait)]
#![feature(unsize)]

use core::marker::Unsize;
use core::fmt::Debug;

pub fn returns_unrelated_impl<T>() -> impl Debug + 'static { () }

returns_unrelated_impl is a simplified version of create_trait. It turns out that it doesn't matter that the function has any arguments at all – all that matters is the generic type parameter (here T as in your example, although the same thing happens with implicit type parameters from impl Trait in argument position).

pub fn test_1<'a>() -> impl Debug {
    returns_unrelated_impl::<&'a ()>()
}

This first test, without a Box, fails for the same reason as in your example (and the same reason as in the post linked in the first comment). Rust has a requirement that an impl Trait in return position should be variant with respect to the generic parameters of the same function, even though this isn't actually necessary to make the type system sound. (There's some potential motivation for this in the linked post.)

However, because this variance requirement isn't required for soundness, the compiler only actually checks it in situations where the type-checker needs to check for variance. Here's an equivalent of the above code using Box<dyn debug>, in which the return value of returns_unrelated_impl is required to be an impl Unsize<dyn Debug> + 'static:

pub fn test_2a<'a>() -> impl Debug {
    type ImplUnsizeStatic = impl Unsize<dyn Debug> + 'static;

    let rv1: ImplUnsizeStatic = returns_unrelated_impl::<&'a ()>();
    let rv2: Box<dyn Debug> = Box::new(rv1);
    rv2
}

This test fails for the same reason as test_1 does: the compiler tries to check whether the impl Debug + 'static variant on T is an impl Unsize<dyn Debug> + 'static that doesn't mention T, and sees that the variance mismatches (even though the requirement for the variance on T is entirely artificial). Changing the code even slightly causes it to now compile:

pub fn test_2b<'a>() -> impl Debug {
    fn verify_impl_unsize_static<T: Unsize<dyn Debug> + 'static>(x: &T) {}

    let rv1 = returns_unrelated_impl::<&'a ()>();
    verify_impl_unsize_static(&rv1);
    let rv2: Box<dyn Debug> = Box::new(rv1);
    rv2
}

test_2a checked whether rv1 was an impl Unsize<dyn Debug> + 'static, whereas test_2b checks whether rv1 implements Unsize<dyn Debug> + 'static. These two statements seem almost identical, but the compiler is interpreting them differently; in test_1 and test_2a I'm trying to coerce an impl Trait type to another impl Trait type and the compiler notices that T is no longer captured, whereas test_2b is simply checking whether the impl Trait type has an appropriate type to implement Unsize<dyn Debug> with a 'static lifetime, and it does.

The reason that boxing fixes the original problem is the same reason that test_2b works: the requirement to coerce a Box<T> to a Box<dyn Debug> is that T implements Unsize<dyn Debug> + 'static, so the coercion to Box<dyn Debug> is checking precisely the same thing that verify_impl_unsize_static does – it's checking to see whether the return value of returns_unrelated_impl implements a trait rather than whether it can be coerced to a given impl type, and the artificial requirement to capture the generic type parameter seems to be checked only in the latter case.

For what it's worth, I consider this behaviour very surprising: it doesn't make too much sense from a programmer's point of view that test_2a and test_2b behave differently, and this makes me suspect that the intended requirement for an impl Trait type in return position to capture the function's generic parameters has been implemented only partially/incompletely. Although the examples above probably explain the current behaviour, I would be very surprised if the current behaviour were what the authors of the language actually intended the compiler to do.

ais523
  • 657
  • 4
  • 8