Using lifetimes to distance bounds checking from actual write


#1

I’m trying to write a wrapper for a slice that allows bounds checking and actual writing to the slice be distanced from each other so that other processing can happen in between.

The way am trying to accomplish this is to have the bounds checking method on the wrapper return a handle that mutably borrows the wrapper itself so that no other calls on the wrapper can be made as long as the handle is alive. Then, I’m trying to make the method on the handle that actually does the writing consume the handle itself in order to make the handle no longer be alive after the writing. At this point, I’d expect to be able to obtain a new handle from the wrapper, since the wrapper is no longer mutably borrowed.

The code below compiles. However, when I uncomment the loop, the compiler complains that I can’t borrow the wrapper again, because the previous borrow ends at the end of the function.

Clearly, I misunderstanding some basics.

Why doesn’t the borrow end at the end of the loop body so that the next iteration could do a new borrow? Is there some existing code that I should look at that implements the kind of pattern that I’m trying to implement per the description above?

struct Handle<'a> {
    dest: &'a Destination<'a>,
}

impl<'a> Handle<'a> {
    fn new(dst: &'a mut Destination<'a>) -> Handle<'a> {
        Handle { dest: dst }
    }
    fn write(mut self, bmp: u16) {
        // Eventually write something unsafe here that writes via dest to its
        // wrapped slice without bounds checking and increment pos.
    }
}

struct Destination<'a> {
    slice: &'a [u16],
    pos: usize,
}

impl<'a> Destination<'a> {
    fn new(dst: &mut [u16]) -> Destination {
        Destination { slice: dst, pos: 0 }
    }
    fn check_space(&'a mut self) -> Option<Handle<'a>> {
        if self.pos < self.slice.len() {
            Some(Handle::new(self))
        } else {
            None
        }
    }
}

fn main() {
    let mut v = vec![1u16, 2u16, 3u16];
    let mut d = Destination::new(&mut v[..]);
    //loop {
        match d.check_space() {
            None => {
                println!("No space");
                return;
            },
            Some(h) => {
                // do something else here
                h.write(0u16)
            },
        }
    //}
    println!("End");
}

#2

First, the code:

struct Handle<'a, 'b> where 'b: 'a {
    dest: &'a Destination<'b>,
}

impl<'a, 'b> Handle<'a, 'b> where 'b: 'a {
    fn new(dst: &'a mut Destination<'b>) -> Handle<'a, 'b> {
        Handle { dest: dst }
    }
    fn write(self, _bmp: u16) {
        // Eventually write something unsafe here that writes via dest to its
        // wrapped slice without bounds checking and increment pos.
    }
}

struct Destination<'a> {
    slice: &'a mut [u16],
    pos: usize,
}

impl<'a> Destination<'a> {
    fn new(dst: &mut [u16]) -> Destination {
        Destination { slice: dst, pos: 0 }
    }
    fn check_space<'b>(&'b mut self) -> Option<Handle<'b, 'a>> {
        if self.pos < self.slice.len() {
            Some(Handle::new(self))
        } else {
            None
        }
    }
}

fn main() {
    let mut v = vec![1u16, 2u16, 3u16];
    let mut d = Destination::new(&mut v[..]);
    // loop {
        match d.check_space() {
            None => {
                println!("No space");
                return;
            },
            Some(h) => {
                // do something else here
                h.write(0u16)
            },
        }
    // }
    println!("End");
}

First, note that I changed slice to a mutable pointer, since your intention was to write through it.

Anyway, your actual problem was that you were tying the lifetime 'a, which was part of Destination to the duration of the borrow made to call check_space. You use 'a in both positions, meaning they have to be the same.

To put it in other words: you were requiring that the borrow for check_space last exactly as long as the borrow used for slice lasts, and that borrow has to last for at least as long as d is valid… which is the entire function.

Decoupling the two lifetimes allows things to work. This allows the compiler to reason about the lifetime of the Destination borrow and the lifetime of the slice borrow independently.


#3

First, note that I changed slice to a mutable pointer, since your intention was to write through it.

The Rust Book’s treatment of the struct field-level mutability confused me. I thought that the reference became mut thanks to the struct itself getting used in a mut context. I’ll file a bug on the book to request this be clarified.

Decoupling the two lifetimes allows things to work.

Thank you! Now this seems to work the way I wanted.


#4

The field inherits mutability, not the pointer. &T and &mut T are different types; they don’t magically change based on context.


#5

I filed an enhancement request on the book.


#6

I’ve written a larger exploration of sound unchecked indexing using “branding” with lifetimes: indexing. It’s quite cool, and it works well on certain algorithms. It has both a trusted Index type and a trusted Range type.