Lifetime problem when impl trait-associated type

use tokio::io::{self, AsyncRead, AsyncWrite};
struct MessageProxy<M: AsRef<[u8]>> {
    message: M,
}

impl<M: AsRef<[u8]>> MessageProxy<M> {
    pub fn new(message: M) -> Self {
        MessageProxy { message }
    }
}

impl<M: AsRef<[u8]>> Honeypot for MessageProxy<M> {
// Problem is here. 
    type Connection = MessageConn;  
    fn connect(&mut self) -> Self::Connection {
        MessageConn {
            reader: self.message.as_ref(),
            writer: io::sink(),
        }
    }
}
struct MessageConn<'a> {
    reader: &'a [u8],
    writer: io::Sink,
}

impl AsyncRead for MessageConn<'_> {
   ...
}
impl AsyncWrite for MessageConn<'_> {
     ...
}

The problem is in this line type Connection = MessageConn. As we know, if fn connect(&mut self)-> MessageConn is a struct method instead of a trait method, then the compiler will assume the lifetime of the output(MessageConn) binds to the input(self). However, here, the compiler complains that I didn't specify a lifetime. Does it mean I must define the trait with Generic Associated Type(GAT)?

trait Honeypot {
    type Connection<'conn> where Self: 'conn;
    fn connect(&mut self) -> Self::Connection<'_>;
}

impl<M: AsRef<[u8]>> Honeypot for MessageProxy<M> {
    type Connection<'conn> = MessageConn<'conn> where M: 'conn;
    fn connect(&mut self) -> Self::Connection<'_> {
        MessageConn {
            reader: self.message.as_ref(),
            writer: io::sink(),
        }
    }
}

Rust Playground

3 Likes

This may be a case of one of Rust's syntactical missteps being misleading. When you have

// A struct with a lifetime parameter
struct MessageConn<'a> { /* ... */ }
// And this function signature
fn connect(&mut self) -> MessageConn { /* ... */ }

The function signature is short for

fn connect(&mut self) -> MessageConn<'_> { /* ... */ }
// This part of the API is crucial  ^^^^

And the elided part is the part that binds the lifetime of the output to the input; the "binding" is only possible because the struct has a lifetime parameter.

In order to prevent yourself from hiding the crucial information, I recommend using the #[deny(elided_lifetimes_in_paths)] lint.


When the struct does not have a lifetime parameter,

struct Connection { /* ... */ }
fn connect(&mut self) -> Connection { /* ... */ }

there is nothing connecting the input borrow to the returned struct -- structs without lifetime parameters don't have a "place" to implicitly connect the lifetime (i.e. one of those '_ things) to. So there can't be a lifetime connection between the input borrow and the output type.

This is also true for things like type aliases and associated types; if they're supposed to capture the lifetime of the &'_ mut self, they must also be parameterized by a lifetime.[1] So a GAT is one way to solve the OP, as @vague demonstrated. Since you seem to control the trait, it's probably what you want.


I'll keep going a little further anyway, though.

The error you received was about the associated type definition, not the method signature. The problem is that you must choose a fully resolved type for the (non-generic) associated type -- one where any generic parameters, including lifetimes parameters, have been given specific values. The only lifetimes you can name in this position are

  • Those parameterizing the trait (in this case there are none)
  • Those parameterizing the implementing type (in this case there are none)
  • 'static (which doesn't fit your use case)
  • (those parameterizing the GAT, in the case of GAT)

If you couldn't change the trait for some reason, another potential solution workaround for the OP would be to implement the trait for &'a MessageProxy<M> so that the implementing type does have a lifetime. This is feasible because you don't actually mutate anything in the method body, and because you can (effectively) copy the shared reference with lifetime 'a out from behind the &mut self, and thus return a MessageConn<'a> (with a lifetime longer than that of the &'short mut self). Example.

But since you do control the trait, this is probably inferior to the GAT solution.


  1. Or utilize some other feature that can annotate lifetimes, like return-position impl Trait. ↩ī¸Ž

3 Likes

Thanks for your elaborate reply! The "going further" pieces help me a lot! Alternatively, to keep it simple, I chose another workaround for a while to make the MessageConn own the message, which may hinder the performance.

@indirection42 Besides: Lifetime Bounds on a GAT are, according to RFC#1598 also equivalent to HRTBs on the trait itself. See here. :slight_smile:

I know - that's not directly related to your problem. :smiley: ... but IMHO knowing and understanding such things helps a lot getting a deeper understanding about what's actually going on. :slight_smile:

1 Like

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.