Confused about using trait with lifetime as generi

2019-02-27 13:03发布

问题:

I am trying to make some kind of decoder, that will be able to deserialize entries without actually copying memory, just by mapping values to some memory regions. That is what I currently managed to do (simplified for testcase):

#![allow(unstable)]

trait CastAbility: Sized { }
impl CastAbility for u64 { }
impl CastAbility for u32 { }
impl CastAbility for u16 { }
impl CastAbility for u8 { }

trait Cast {
    fn cast<'a>(mem: &'a [u8]) -> Result<&'a Self, String>;
}

impl<T> Cast for T where T: CastAbility {
    fn cast<'a>(mem: &'a [u8]) -> Result<&'a T, String> {
        if mem.len() != std::mem::size_of::<T>() { 
            Err("invalid size".to_string())
        } else {
            Ok(unsafe { std::mem::transmute(mem.as_ptr()) })
        }
    }
}

impl Cast for str {
    fn cast<'a>(mem: &'a [u8]) -> Result<&'a str, String> {
        Ok(unsafe { std::mem::transmute(std::raw::Slice { data: mem.as_ptr(), len: mem.len() }) })
    }
}

trait Read<'a> {
    fn read(mem: &'a [u8]) -> Result<Self, String>;
}

#[derive(Show, PartialEq)]
struct U8AndStr<'a> {
    value_u8: &'a u8,
    value_str: &'a str,
}

impl<'a> Read<'a> for U8AndStr<'a> {
    fn read(mem: &'a [u8]) -> Result<U8AndStr, String> {
        Ok(U8AndStr {
            value_u8: try!(Cast::cast(mem.slice(0, 1))),
            value_str: try!(Cast::cast(mem.slice(1, mem.len()))),
        })
    }
}

fn main() {
    let mem: &[u8] = &[0x01, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37];
    let value: U8AndStr = Read::read(mem).unwrap();

    println!("value: {:?}", value);
}

playpen

In fact it compiles and even works, but now I cannot understand how to use my Read trait as generic parameter. For example, suppose I want to compare a value to a decoded result of some memory area:

fn compare_to_smth<'a, T>(value: &'a T) -> bool where T: PartialEq+Read<'a> {
    let mem: &[u8] = &[0x01, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37];
    let smth_value: T = Read::read(mem).unwrap();
    smth_value == *value
}

fn main() {
    let value = U8AndStr { value_u8: &1, value_str: "01234567" };
    assert!(compare_to_smth(&value));
}

It fails with "borrowed value does not live long enough", and I can guess why: because mem lifetime is the function body, not 'a, as I did specify in signature for input parameter. So I tried to use second lifetime paramater as shown:

fn compare_to_smth<'a, 'b, T>(value: &'a T) -> bool where T: PartialEq+Read<'b> {

But it also didn't work for obvious reason. So I really don't understand how to make compare_to_smth work without passing memory chunk from outside. Is there any solution, or I should refactor the code somehow?

回答1:

Unfortunately, what you want to do is inexpressible in Rust at the moment.

The signature of Read trait that actually would work is as follows (in pseudo-Rust):

trait<'r> Read for Self<'r> {
    fn read<'a>(mem: &'a [u8]) -> Result<Self<'a>, String>;  // '
}

That is, Self must be a higher-kinded type in its lifetime parameter. This requires support for higher-kinded types, which is a very desired feature in the Rust community but which is still yet to be implemented.

The problem with the original signature:

trait Read<'a> {
    fn read(mem: &'a [u8]) -> Result<Self, String>;
}

is that 'a is a parameter to the trait. When this trait is used as a trait bound:

fn compare_to_smth<'a, T>(value: &T) -> bool where T: PartialEq+Read<'a>

it means that the caller of this function chooses the actual lifetime parameter. For example, the caller may choose 'static:

fn compare_to_smth<T>(value: &T) -> bool where T: PartialEq+Read<'static>

However, the function uses &[u8] whose lifetime is not 'static.

In fact, this concrete example may be not exactly correct due to variance (I imagine it would be sound for this lifetime to be 'static here, but variance of lifetimes itself is somewhat confusing, so I'm not really sure), but nevertheless the general idea is the same: in order for this to work the Read::read method must be polymorphic in the lifetime of its argument and result, but you can't write such signature yet.



回答2:

I think the issue might be more in the signature of compare_to_smth.

fn compare_to_smth<'a, T>(value: &'a T) // this implies a T: 'a bound
                       // because otherwise we would not be able to
                       // have a &'a T (references can't live longer
                       // than the thing they reference)

but then inside the function you're doing:

let smth_value: T = Read::read(mem).unwrap(); 
                                 // give me something of type T that
                                 // lives less than T

I may be wrong, but I don't think this depends on how Read is defined and how sophisticated the type system can become in the future, as whatever you write on the right side doesn't change the fact that you're expecting a T on the left side (and T has to outlive 'a).

A "sufficiently smart compiler" might be able to see that smth_value is actually not living longer than 'a and that what you're doing is safe, but in general this would be unsafe. I'm sure that using an unsafe transmute in compare_to_smth completely defeats your purpose, but just to demonstrate, this works:

fn compare_to_smth<'a, T>(value: &'a T) -> bool
    where T: Read<'a> + PartialEq
{
    let mem: &[u8] = &[0x01, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37];
    let smth_value = <U8AndStr as Read>::read(mem).unwrap();
    let vl: &U8AndStr = unsafe{ std::mem::transmute(value) };

    smth_value == *vl
}


标签: rust