Search code examples
rustlifetime

How to implement a trait for both a reference and an owned value?


I have defined the following trait:

trait Read<T> {
    fn next_row<'c>(&'c mut self) -> Result<std::option::Option<T>>;
}

(obviously 'c would be elided but I just want to be able to refer to it later)

I have implemented this for a struct, ReaderA, to return an owned value:

struct ReaderA {
    data: Vec<Vec<u8>>
}

impl Read<Vec<u8>> for ReaderA {
    fn next_row<'c>(&'c mut self) -> Result<std::option::Option<Vec<u8>>> {
        Ok(self.data.pop())
    }
}

Unsurprisingly, this works perfectly. The problem occurs when I try to implement this for some struct ReaderB to return a borrowed value:

struct ReaderB {
    data: Vec<Vec<u8>>
}

impl Read<&Vec<u8>> for ReaderB {
    fn next_row<'c>(&'c mut self) -> Result<std::option::Option<&'c Vec<u8>>> {
        Ok(self.data.first())
    }
}

This doesn't work. The signature doesn't match the definition because the trait doesn't expect 'c to be included in the return value. What am I doing wrong?

I could potentially add the lifetime as a trait generic parameter, however this feels unnecessary: I don't want to specify some other lifetime, I just want it to share the lifetime of the &mut self borrow (I think). I also would far rather not have to include a lifetime in every implementation of the trait and PhantomData in every struct, even when they don't return a borrowed value. I also don't want to run into something like this, but I don't really understand what I'm doing.

I feel this may be something to do with HRTB, however I can't work out how to get that to work.

I am somewhat lost with lifetimes, so please include as much detail as possible in your answer.


Solution

  • Two remarks:

    • Using generics for return values is discouraged, unless you are implementing a conversion function or similar where the user of your trait wants to decide what type it should be. If there is exactly one return type per implementing type, use an associated type instead, like in std::iter::Iterator.
    • The trait definition doesn't capture whether T should have a reference to Self or not. In one case it does, in the other it does not. This is the main problem why your approach fails; if somebody wants to use the trait, he could keep the produced value but destroy the Read object, which should demonstrate why the ReaderB implementation in your example code is rightfully forbidden.

    If you convert the generic to an associated type and annotate in the trait that Self should always outlive the return value, then it works:

    use anyhow::Result;
    
    pub trait Read {
        type Item<'a>
        where
            Self: 'a;
        fn next_row(&mut self) -> Result<std::option::Option<Self::Item<'_>>>;
    }
    
    pub struct ReaderA {
        data: Vec<Vec<u8>>,
    }
    impl Read for ReaderA {
        type Item<'a> = Vec<u8>;
        fn next_row(&mut self) -> Result<std::option::Option<Vec<u8>>> {
            Ok(self.data.pop())
        }
    }
    
    pub struct ReaderB {
        data: Vec<Vec<u8>>,
    }
    impl Read for ReaderB {
        type Item<'a> = &'a Vec<u8>;
        fn next_row(&mut self) -> Result<std::option::Option<&Vec<u8>>> {
            Ok(self.data.first())
        }
    }