Search code examples
rustlifetimeunsafe

Fiddling with lifetimes: sanity check on unsafe reinterpretation of lifetimes


Suppose you have two collections (Vec for simplicity here) of instances of T, and a function to compute whether the elements in those collections appear in either or both of them:

// With lifetimes not yet annotated
fn comm(left: &Vec<T>, right: &Vec<T>) -> Vec<(Tag, &T)> {}

enum Tag {
    Left,
    Both,
    Right,
}

comm(l,r) guarantees that the references returned point to elements of the left collection in both the case that T was present in left only, and T was present in both.

However, because some T might appear in right only, the function's full signature must look like this:

fn comm<'a, 'b, 'c>(left: &'a Vec<T>, right: &'b Vec<T>) -> Vec(Tag, &'c T)
where
  'a: 'c,
  'b: 'c,

The actual question, then: I know that, within one of the (tag, &T) tuples returned comm, if tag == Left or tag == Both, then &T will surely point to the left collection.

Is it sane, safe, and legitimate to use mem::transmute or other mechanism to grab one of the references returned by comm and cast it to the lifetime matching the left collection?

For instance:

fn last_common<'a, 'b>(left: &'a Vec<T>, right: &'b Vec<T>) -> &'a T {
  let tagged = comm(left, right);
  let (tag, ref_to_T) = boring code that picks one tuple from tagged...
  assert!(matches!(tag, Tag::Left) || matches!(tag, Tag::Both))
  return std::mem::transmute::<&'_ T, &'a T>(ref_to_T);
}

Solution

  • Yes, it is sound. In fact, the official documentation for transmute() says it can be used to extend lifetimes:

    https://doc.rust-lang.org/stable/std/mem/fn.transmute.html#examples

    Extending a lifetime, or shortening an invariant lifetime. This is advanced, very unsafe Rust!

    struct R<'a>(&'a i32);
    unsafe fn extend_lifetime<'b>(r: R<'b>) -> R<'static> {
        std::mem::transmute::<R<'b>, R<'static>>(r)
    }
    
    unsafe fn shorten_invariant_lifetime<'b, 'c>(r: &'b mut R<'static>)
                                                 -> &'b mut R<'c> {
        std::mem::transmute::<&'b mut R<'static>, &'b mut R<'c>>(r)
    }
    

    But I will not recommend it. Instead, I'll recommend you to use an enum:

    fn comm<'a, 'b, T>(left: &'a Vec<T>, right: &'b Vec<T>) -> Vec<Tag<'a, 'b, T>> {}
    
    enum Tag<'a, 'b, T> {
        Left(&'a T),
        Both(&'a T), // Could be `&'b T`, too.
        Right(&'b T),
    }
    

    You can also have a method to extract the value with the shorter lifetime, like:

    impl<'a, T> Tag<'a, 'a, T> {
        pub fn value(self) -> &'a T {
            let (Self::Left(v) | Self::Right(v) | Self::Both(v)) = self;
            v
        }
    }