Generic trait parameters instead of life-time parameters: necessary for taking ownership?


#1

At first glance, it looks as if std::io::Bytes could be parameterized over a lifetime instead of over a type, like this:

use std::io::{Read, Result};

struct Bytes<'a> {
    inner: &'a mut Read,
}

impl<'a> Iterator for Bytes<'a> {
    type Item = Result<u8>;

    fn next(&mut self) -> Option<Result<u8>> {
        let mut buf = [0];
        match self.inner.read(&mut buf) {
            Ok(0) => None,
            Ok(..) => Some(Ok(buf[0])),
            Err(e) => Some(Err(e)),
        }
    }
}

impl<'a> Bytes<'a> {
    fn new(r: &'a mut Read) -> Bytes<'a> {
        return Bytes {inner: r};
    }
}

The advantage would be more code sharing as the result of lifetime erasure.

Why is Bytes not implemented this way? It is probably not a performance concern because Bytes.next() is really quite slow even if backed by a BufReader).

Is the reason that it would impossible to write a function like this?

use std::fs::File;
    
fn iter_open(path: &std::path::Path) -> Result<std::io::Bytes<File>> {
    Ok(File::open(path)?.bytes())
}

I think the language still does not provide a way to construct a File object locally in iter_open which is guaranteed to outlive the returned Bytes object, and this is true even if we changed the return type to something which embeds a Bytes field.


#2

How would it? Anything that is constructed is on the stack and owned by the function. So, now you got two options: either let something else take ownership (e.g. Bytes) or return it along with it along with the reader. Otherwise, it would be dropped by the end of the function. You’d have to somehow allow borrows to extend the lifetime of things they point to, which Rust doesn’t do. Lifetimes are descriptive.

Also, what you wrote isn’t remotely equivalent: &'a mut Read expresses a trait object. This brings along with it dynamic dispatch, which you probably don’t want in this case, even if it is already slow.


#3

I meant that it is impossible to work around this even with a heap allocation. Once you have a type with a lifetime parameter, you can’t put that into a struct, so it’s an impediment to constructing further abstractions.

I don’t know if the dispatching overhead will ever make a difference here. The problem is the heavy result type (Option<Result<u8>>), which is fairly expensive to construct and dissect. Of course, for some readers, inlining is theoretically possible, but e.g. for Bytes<BufReader>, what seems to happen that is that read_one_byte is inlined into the caller. But to make the iteration fast, the fast path with BufRead::read() would have to be inlined, so that no fat result object has to be constructed at all if there is still some data in the buffer.


#4

Maybe I’m misunderstanding your point, but you can put a lifetime parameterized type as a field in a struct - the struct just has to gain a lifetime parameter itself, unless the type you’re embedding can be instantiated with a 'static lifetime. Think of it like a generic type - you can only put it into another struct if that other struct has a type parameter for it (i.e. is generic itself) or it instantiates the type with a concrete type. Lifetimes are similar except the only concrete lifetime you can use is 'static.