Trait associated type lifetime issues

I have issues creating the correct function signature when working with a trait that is generic over a lifetime. Playground link
This is what the trait looks like (constructed based on idea here: https://stackoverflow.com/a/33756123)

trait MyTrait<'a> {
    type MyType;

    fn get_my_type(&'a mut self) -> Self::MyType;
}

And this is the function that does not compile:

fn func2<'a, T: MyTrait<'a>>(mut a: T) {
    a.get_my_type();
}

I was wondering if there is any way to fix the function signature to make it compile?
Currently all that I managed to do is either:

  • Use the concrete type as argument (which defeats the whole purpose of the trait abstraction)
  • Change the function's signature to accept a mutable reference to T instead of taking ownership of T. Maybe this could somehow be enough for my project? I have a hunch that somewhere along the way I might need to take ownership over object.

Compiler error:

error[E0309]: the parameter type `T` may not live long enough
  --> src/main.rs:44:5
   |
43 | fn func2<'a, T: MyTrait<'a>>(mut a: T) {
   |              -- help: consider adding an explicit lifetime bound...: `T: 'a +`
44 |     a.get_my_type();
   |     ^ ...so that the type `T` is not borrowed for too long

error[E0309]: the parameter type `T` may not live long enough
  --> src/main.rs:44:7
   |
43 | fn func2<'a, T: MyTrait<'a>>(mut a: T) {
   |              -- help: consider adding an explicit lifetime bound...: `T: 'a +`
44 |     a.get_my_type();
   |       ^^^^^^^^^^^ ...so that the reference type `&'a mut T` does not outlive the data it points at

For reference, I include a full self contained example:

trait MyTrait<'a> {
    type MyType;

    fn get_my_type(&'a mut self) -> Self::MyType;
}

struct MyStruct {
    num: u32,
}

struct MyTypeStruct<'a> {
    num: &'a mut u32,
}

impl<'a> MyTrait<'a> for MyStruct {
    type MyType = MyTypeStruct<'a>;

    fn get_my_type(&'a mut self) -> Self::MyType {
        MyTypeStruct { num: &mut self.num }
    }
}

// Compiles when specifying the struct name directly.
fn func1(mut a: MyStruct) {
    a.get_my_type();
}

// Does not compile:
fn func2<'a, T: MyTrait<'a>>(mut a: T) {
    a.get_my_type();
}

fn main() {
    let mut my_struct = MyStruct { num: 3 };
    let my_type_struct = my_struct.get_my_type();
    *my_type_struct.num = 4;
    println!("{}", my_struct.num);
}

Edit
Here as an example for a function that borrows T instead of taking ownership:

fn func3<'a, T: MyTrait<'a>>(a: &'a mut T) {
    a.get_my_type();
}

It compiles, but I am pretty sure at some point, possibly at one of the callers, I will need to take ownership over the generic T.

The problem is that the lifetime parameter of func2 does not match up with the lifetime of the borrow of the local variable a. In particular, lifetime parameters such as 'a will always refer to lifetimes that are longer than the duration of the function call, whereas the local variable can only be borrowed for shorter than the duration of the function call.

The solution that Rust offers is HRTBs (higher rank trait bounds). Page in the nomicon. Section in the reference. You just assert the trait bound T: MyTrait<'a> for all lifetimes 'a, and that way the lifetime of the short, local borrow is also applicable. The signature becomes

fn func2<T: for<'a> MyTrait<'a>>(mut a: T) {
    a.get_my_type();
}


// alternative syntax with the `for` coming first, in where clauses
fn func2<T>(mut a: T)
where
    for<'a> T: MyTrait<'a>
// you can still also write
//  T: for<'a> MyTrait<'a>
// instead
{
    a.get_my_type();
}
3 Likes

This is something that GATs will make more ergonomic. In the meantime, I recommend the following setup (which is basically akin to Deserialize<'de> / DeserializeOwned in serde):

/// Trait used to write the `impl`s
trait MyTrait_<'a> {
    type MyType /* : bounds… */ ; // You will probably need to add bounds here when doing HRTB…

    fn get_my_type (self: &'a mut Self)
      -> Self::MyType
    ;
}

/// Trait alias to be used in bounds.
trait MyTrait
where
    for<'any> Self : MyTrait_<'any>,
{}
    impl<T : ?Sized> MyTrait for T
    where
        for<'any> Self : MyTrait_<'any>,
    {}
  • GAT version
    trait MyTrait {
        type MyType<'a>;
    
        fn get_my_type<'a> (self: &'a mut Self)
          -> Self::MyType<'a>
        ;
    }
    

That way, you can write:

struct MyStruct {
    num: u32,
}

struct MyTypeStruct<'a> {
    num: &'a mut u32,
}

impl<'a> MyTrait_<'a> for MyStruct {
    type MyType = MyTypeStruct<'a>;

    fn get_my_type (self: &'a mut MyStruct)
      -> MyTypeStruct<'a>
    {
        MyTypeStruct { num: &mut self.num }
    }
}

fn func2<T : MyTrait> (mut a: T)
{
    a.get_my_type();
}

fn func1 (a: MyStruct)
{
    func2(a) // OK
}
2 Likes

@steffahn your solution works. Is there a way to specify the lifetime requirement in the associated type itself? The gap in lifetimes is apparent in the trait definition.

In case that wasn’t clear, what @Yandros is suggesting is to introduce a shorthand/alias for the HRTB for<'a> MyTrait<'a>. Furthermore the suggestion was to name this shorthand MyTrait and name the original trait MyTrait_ instead.

The

trait MyTrait
where
    for<'any> Self : MyTrait_<'any>,
{}

definition is equivalent to

trait MyTrait: for<'a> MyTrait<'a>
{}

so the HRTB becomes a “supertrait”, hence T: MyTrait bounds in a function imply that you have T: for<'a> MyTrait_<'a> available. The two bounds MyTrait and for<'a> MyTrait<'a> become fully equivalent by the additional “blanket implementation”

    impl<T : ?Sized> MyTrait for T
    where
        for<'any> Self : MyTrait_<'any>,
    {}

which is equivalent to

impl<T: ?Sized> MyTrait for T
where
    T: for<'a> MyTrait_<'a>,
{}

this way, T: for<'a> MyTrait_<'a> also implies T: MyTrait.

This is analogous to what serde does, in the case of serde, DeserializeOwned is a shorthand for for<'de> Deserialize<'de>. You can see the supertrait and the blanket implementation in the docs (expand “Show declaration” to see supertraits).

2 Likes

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.