No static lifetime error when using to_vec() directly

The code below compiles and runs but if I change Client::new() as indicated in the comment then I get an error (E0759) regarding the 'static lifetime.

  1. Shouldn't Client::new() reject the argument as it does not live for the entire duration of the program?

  2. The field in Client is of type Conn<'static> so why does the code compile when using c.to_vec().into() if c does not live as long as 'static?

  3. Why does it not compile when using c.clone() if Conn's clone implementation also calls to_vec()?

Thanks in advance!

#[derive(Debug)]
enum Conn<'a> {
    Vec(Vec<u8>),
    Ref(&'a [u8]),
}

impl<'a> AsRef<[u8]> for Conn<'a> {
    fn as_ref(&self) -> &[u8] {
        match self {
            Conn::Vec(v) => v.as_ref(),
            Conn::Ref(r) => r,
        }
    }
}

impl<'a> From<Vec<u8>> for Conn<'a> {
    fn from(v: Vec<u8>) -> Self {
        Self::Vec(v)
    }
}

impl<'a> Clone for Conn<'a> {
    #[inline]
    fn clone(&self) -> Self {
        Self::from(self.as_ref().to_vec())
    }
}

impl<'a> std::ops::Deref for Conn<'a> {
    type Target = [u8];

    #[inline]
    fn deref(&self) -> &[u8] {
        match self {
            Conn::Vec(v) => v.as_ref(),
            Conn::Ref(v) => v,
        }
    }
}

#[derive(Debug)]
struct Client {
    c: Conn<'static>
}

impl Client {
    fn new(c: &Conn) -> Self {
        Self { c: c.to_vec().into() }
    }
}

// this produces error E0759
// impl Client {
//     fn new(c: &Conn) -> Self {
//         Self { c: c.clone() }
//     }
// }

fn main() {
    let c = Conn::Vec(Vec::new());
    let client = Client::new(&c);
    println!("client: {:?}", client);
}

  1. Client::new() first clones the borrowed slice into an owned Vec<u8> using c.to_vec(). Then, in order to call into(), it uses the impl<'a> From<Vec<u8>> for Conn<'a> impl. Setting 'a to 'static, this allows it to create a Conn<'static> from a Vec<u8>.
  2. c does not live as long as static, but c.to_vec() is an owned Vec<u8> that is independent from the lifetime of c. Note that to_vec() creates a new and independent copy of the original slice.
  3. If c is a Conn<'a>, then c.clone() is another Conn<'a>, since Clone::clone() always produces the exact same type. In contrast, c.to_vec().into() can produce a Conn with any lifetime whatsoever, since the From<Vec<u8>> impl has an unbounded lifetime 'a.
1 Like
  1. Client::new() first clones the borrowed slice into an owned Vec<u8> using c.to_vec() . Then, in order to call into() , it uses the impl<'a> From<Vec<u8>> for Conn<'a> impl. Setting 'a to 'static , this allows it to create a Conn<'static> from a Vec<u8> .

Why/how does the lifetime of Conn get set to 'static in impl<'a> From<Vec<u8>> for Conn<'a> impl? How does that work?

  1. If c is a Conn<'a> , then c.clone() is another Conn<'a> , since Clone::clone() always produces the exact same type . In contrast, c.to_vec().into() can produce a Conn with any lifetime whatsoever, since the From<Vec<u8>> impl has an unbounded lifetime 'a .

I see so the lifetime gets clone as well.

  1. c does not live as long as static, but c.to_vec() is an owned Vec<u8> that is independent from the lifetime of c . Note that to_vec() creates a new and independent copy of the original slice.

Makes sense. And Rust is able to tell that the variant used here is Conn::Vec which owns its data and not the other variant which holds a reference which would need a reference with 'static lifetime, right?

This is known as lifetime substitution. When you write c.to_vec().into(), the compiler starts searching for a method fn into(self) -> Conn<'static> defined on Vec<u8>. (It infers the required return type from the surrounding context.) It sees that the only possible into() method is Into::into(), which is available if and only if Conn<'static> implements From<Vec<u8>>.

At this point, it finds your own impl<'a> From<Vec<u8>> for Conn<'a>. Since the impl has an 'a lifetime with no additional restrictions, it sees that Conn<'a> implements From<Vec<u8>> for every possible lifetime 'a in existence. Since this includes the 'static lifetime, the resolution succeeds: Conn<'static> implements From<Vec<u8>>, so Vec<u8> implements Into<Conn<'static>>, so a matching into() method exists.

No, the compiler does not try to determine this. Instead, it sees that Conn has no inherent method named to_vec(). But it does have a Deref impl, so it applies an "autoderef" transformation, effectively turning it into c.deref().to_vec().into(). First, deref() borrows &c and returns an &[u8] slice. But the lifetime of this slice is not the lifetime of the Conn; instead, it is the very short lifetime of the specific &c borrow. Then, to_vec() takes this short-lived &[u8] slice, and copies all of its bytes into an owned Vec<u8>. It performs this copy regardless of the variant of c, since at this point, the slice is only borrowed from c either way. Finally, into() takes the copied Vec<u8> and places it into Conn::Vec, due to your From<Vec<u8>> impl.

If you don't want to copy the bytes if c is a Conn::Vec, then consider adding an into_vec() method that matches over it:

impl<'a> Conn<'a> {
    fn into_vec(self) -> Vec<u8> {
        match self {
            Self::Vec(v) => v,
            Self::Ref(r) => r.into_vec(),
        }
    }
}

Also, note that your Conn<'a> is equivalent to a Cow<'a, [u8]> from the standard library. You might consider simply defining Conn as a type alias for Cow, unless you're looking for additional type safety.

2 Likes

Thanks so much for the help!