Borrowing from a local variable, by a generic argument

Hi,

I have the following non-compilable code:

trait MyTraitAcc {
    fn call<'a>(&mut self, item: &'a str) -> ();
}

struct MyStructAcc<'a> {
    vec: Vec<&'a str>,
}

impl<'d> MyTraitAcc for MyStructAcc<'d> {
    fn call<'a>(&mut self, item: &'a str) -> () {
        self.vec.push(item);
    }
}

fn caller<'b, F>(f: F) -> () where F: MyTraitAcc {
    let s2 = String::from("hi");
    f.call(&s2);
}

fn main() {
    caller(MyStructAcc { vec: vec![] });
}

My goal here is to let f hold references to local variables in caller().

Of course, this doesn't work, because in MyStructAcc::call(), there's no constraint that self.vec should outlive item.

Alternatively, I've tried this:

trait MyTraitAcc<'a> {
    fn call(&mut self, item: &'a str) -> ();
}

struct MyStructAcc<'a> {
    vec: Vec<&'a str>,
}

impl<'d> MyTraitAcc<'d> for MyStructAcc<'d> {
    fn call(&mut self, item: &'d str) -> () {
        self.vec.push(item);
    }
}

fn caller<'b, F>(mut f: F) -> () where F: MyTraitAcc<'b> {
    let s2 = String::from("hi");
    f.call(&s2);
}

fn main() {
    caller(MyStructAcc { vec: vec![] });
}

But, now, in caller(), f will outlive s2 (unnecessarily, I think).

Is there any way to make this work? Or is there a story about why Rust wants this to be impossible? I've tried a few minor variations, but I always run into roadblocks. I can only seem to make it work if I get rid of generics, and deal directly with the struct.

Your second approach is the one that’s more reasonable. Still it’s hard to convince Rust to put a short-lived thing into a collection that already existed beforehand.

You can use variance to help you out though. A MyStructAcc<'a> can be reinterpreted as a MyStructAcc<'b> if 'b is shorter than 'a (i.e. 'a: 'b), so the main function can produce a MyStructAcc<'a> and the caller function can turn it into MyStructAcc<'b> where 'b is the lifetime of the borrow of the local variable. Here, see, this compiles:

fn convert<'a: 'b, 'b>(x: MyStructAcc<'a>) -> MyStructAcc<'b> {
    x
}

And you could use this kind of conversion in a non-generic setting like so:

trait MyTraitAcc<'a> {
    fn call(&mut self, item: &'a str) -> ();
}

struct MyStructAcc<'a> {
    vec: Vec<&'a str>,
}

impl<'d> MyTraitAcc<'d> for MyStructAcc<'d> {
    fn call(&mut self, item: &'d str) -> () {
        self.vec.push(item);
    }
}

fn caller<'b>(f: MyStructAcc<'b>) {
    let mut f = f; // turns MyStructAcc<'b> into MyStructAcc<'shorter>
    let s2 = String::from("hi");
    f.call(&s2);
}

fn main() {
    caller(MyStructAcc { vec: vec![] });
}

(playground)

It becomes a bit harder since you’re using a trait and writing a generic function. You would need to add some methods to the trait in order to do the conversion. Something like

trait MyTraitAcc<'a> {
    fn call(&mut self, item: &'a str);
    fn shorten_lifetime<'b>(self) -> impl MyTraitAcc<'b>
    where
        'a: 'b;
}

Except that isn’t valid Rust syntax, impl Trait isn’t supported in traits. But we can work towards encoding it differently. The impl MyTraitAcc<'b> would need to be an associated type…

trait MyTraitAcc<'a> {
    fn call(&mut self, item: &'a str);
    type WithShortenedLifetime<'b>: MyTraitAcc<'b>;
    fn shorten_lifetime<'b>(self) -> Self::WithShortenedLifetime<'b>
    where
        'a: 'b;
}

except, generic associated items aren’t a thing yet either. But you can instead add a higher-ranked bound on a supertrait that has 'b as a parameter:

trait MyTraitAcc<'a>: for<'b> WithShortenedLifetimeTrait<'b> {
    fn call(&mut self, item: &'a str);
    fn shorten_lifetime<'b>(self) -> <Self as WithShortenedLifetimeTrait<'b>>::WithShortenedLifetime
    where
        'a: 'b;
}
trait WithShortenedLifetimeTrait<'b> {
    type WithShortenedLifetime: MyTraitAcc<'b>;
}

and

impl<'b> WithShortenedLifetimeTrait<'b> for MyStructAcc<'_> {
    type WithShortenedLifetime = MyStructAcc<'b>;
}

Adding the implementation:

impl<'d> MyTraitAcc<'d> for MyStructAcc<'d> {
    fn call(&mut self, item: &'d str) {
        self.vec.push(item);
    }
    fn shorten_lifetime<'b>(self) -> MyStructAcc<'b>
    where
        'd: 'b,
    {
        self
    }
}

and using it

fn caller<'b, F>(f: F)
where
    F: MyTraitAcc<'b>,
{
    let mut f = f.shorten_lifetime();
    let s2 = String::from("hi");
    f.call(&s2);
}

almost works!

   Compiling playground v0.0.1 (/playground)
error[E0597]: `s2` does not live long enough
  --> src/main.rs:40:12
   |
40 |     f.call(&s2);
   |            ^^^ borrowed value does not live long enough
41 | }
   | -
   | |
   | `s2` dropped here while still borrowed
   | borrow might be used here, when `f` is dropped and runs the destructor for type `<F as WithShortenedLifetimeTrait<'_>>::WithShortenedLifetime`
   |
   = note: values in a scope are dropped in the opposite order they are defined

error: aborting due to previous error

The only problem left is the drop order isn’t making the compiler happy. It wants s1 dropped after f. It was happy with this in the non-generic case because Vec is known to the compiler to do nothing bad to dangling references it’s containing in case it’s dropped a bit later than what the references pointed to. We can fix this issue either by swapping their definition

fn caller<'b, F>(f: F)
where
    F: MyTraitAcc<'b>,
{
    let s2 = String::from("hi");
    let mut f = f.shorten_lifetime();
    f.call(&s2);
}

or inserting a manual drop

fn caller<'b, F>(f: F)
where
    F: MyTraitAcc<'b>,
{
    let mut f = f.shorten_lifetime();
    let s2 = String::from("hi");
    f.call(&s2);
    drop(f);
}

(here’s a playground link)

I’ll admit, the names of the new supertrait and associated type could probably be improved.

3 Likes

Thank you for the detailed response - this is fantastic!

I do have a couple of follow-up questions. The first is about the anonymous lifetime on a struct:

impl<'b> WithShortenedLifetimeTrait<'b> for MyStructAcc<'_> {
    type WithShortenedLifetime = MyStructAcc<'b>;
}

What, exactly, does '_ refer to here?

Second - really just out of curiosity - is there a fundamental/logical reason that impl Trait isn't supported in traits? (I haven't read through RFC 1522 or surrounding docs. - maybe the answer is there.)

Thanks again.

The '_ in general is “elided lifetime”. It’s the explicit syntax for writing no explicit lifetime at all. For references &'_ T is the same as writing &T. What exactly elided lifetimes mean differs, depending on where they appear. In the case of function signatures there are special elision rules. The '_ lifetime can also be used in an expression for a type annotation of a let, an as cast or a type argument when it means something like _ for types, i.e. “not further specified” or “the compile shall infer…”.

In case of trait implementations, it’s similar to the elision rules for function arguments. It implicitly introduces a new lifetime argument for each '_. This means that

impl<'b> WithShortenedLifetimeTrait<'b> for MyStructAcc<'_> {
    type WithShortenedLifetime = MyStructAcc<'b>;
}

is just a shorthand for

impl<'a, 'b> WithShortenedLifetimeTrait<'b> for MyStructAcc<'a> {
    type WithShortenedLifetime = MyStructAcc<'b>;
}

it’s all related. As I demonstrated above, the most direct/natural translation involves GATs (generic associated types). But GATs don’t exist (yet) either. Going the other way, async fn does basically just desugar to an fn returning impl Future<…>. So in this sense, async methods in traits are in the same design space. I’m fairly positive that we’ll eventually have support for all of these in Rust, but the design work has to be done and all potential issues resolved before it can be stabilized.

By the way, GATs are implemented and available on nightly as an unstable feature (example in playground), so the

trait MyTraitAcc<'a> {
    fn call(&mut self, item: &'a str);
    type WithShortenedLifetime<'b>: MyTraitAcc<'b>;
    fn shorten_lifetime<'b>(self) -> Self::WithShortenedLifetime<'b>
    where
        'a: 'b;
}

approach is actually already possible, at least using unstable features.

2 Likes

Thank you again - I greatly appreciate your very clear explanation!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.