How to create a slice-like type?

I want to create my custom PathBuf type which should essentially behave like a Vec. I also want to be able to create slices thereof but I fail to create such a type.

type PathSegment = ();

struct PathSlice([PathSegment]);
struct PathBuf(Vec<PathSegment>);

impl std::convert::AsRef<PathSlice> for PathBuf {
    
    fn as_ref(&self) -> &PathSlice {
        &PathSlice(self.0[..])
    }
}

This fails because of unsizedness.

My other approach using references with lifetimes fails with “lifetime parameters or bounds on method as_ref do not match the trait declaration” - most probably because I don’t know exactly what I’m doing there:

type PathSegment = ();

struct PathSlice<'a>(&'a [PathSegment]);
struct PathBuf(Vec<PathSegment>);

impl<'a> std::convert::AsRef<PathSlice<'a>> for PathBuf {
    
    fn as_ref<'b>(&'b self) -> &'b PathSlice<'a> where 'b: 'a {
        &PathSlice(&self.0[..])
    }
}

You will usually have to use unsafe code to do this. I haven’t dabbled in unsized stuff too much, so I can’t really help on that front.

One thing I do remember is that you can do this, but it won’t work in this case.

1 Like

As @KrishnaSannasi mentioned, one typically needs unsafe code:

impl std::convert::AsRef<PathSlice> for PathBuf {
    fn as_ref(&self) -> &PathSlice {
        unsafe { &*(self.0.as_ref() as *const [_] as *const PathSlice) }
       // or similar transmute() version
    }
}

You’ll want to #[repr(transparent)] PathSlice as well.

Test your real code thoroughly though :slight_smile:

4 Likes

Having a Wrapper(T,), from a &Wrapper you can get a &T, thanks to being able to project a reference to a struct into a reference to one of its fields (e.g., &wrapper.0).

However, in general, you cannot go the other way around: that is, going from a &T to a &Wrapper, since nothing guarantees the existence of the “wrapping bread” around the pointee T.

An example of this is that your PathSlice could be / become (e.g., after a refactoring):

type PathSegments = [PathSegment];

struct PathSlice (bool, PathSegments);
// or similarly
enum PathSlice {
    A(PathSegments),
    B(PathSegments),
}

Here we can clearly see why it is not possible to get a &PathSlice from a &PathSegments (and with the type alias it should be easier to spot that unsizedness is not the problem here; you would have had a “cannot return a reference to a local / the local does not live long enough” error in your example): nothing guarantees that a valid bool precedes the PathSegments (and that is without talking into account alignment & padding).

The general solution is to wrap the reference, like you tried:

#![deny(elided_lifetimes_in_paths)]

struct PathSlice<'pathbuf> (
    &'pathbuf PathSegments,
);

impl PathBuf {
    fn as_slice (self: &'_ PathBuf) -> PathSlice<'_>
    {
        PathSlice(&self.0)
    }
}

But this is not compatible with AsRef::as_ref signature (and would require a second wrapper for the &mut case).

The solution, for you case, is to assert (making the compilation fail when it is not the case, hence guarding against such refactoring) that your wrapper only exists at the type level; that on runtime both the wrapper and the wrappee are structurally equivalent; that your wrapper is a transparent wrapper. Hence the need for the #[repr(transparent)] attribute:

#[repr(transparent)]
struct Wrapper /* = */ (
    Wrappee,
);

With that attribute, it becomes sound (although it still requires unsafe; I guess that a proc_macro_attribute granting a non-unsafe transmuting API would be a nice thing to do) to transmute between Wrapper and Wrappee, or between any pointer to Wrapper and the same pointer to Wrappee:

  • Wrapper = Wrappee,

  • &'a Wrapper = &'a Wrappee (and *const _),

  • &'a mut Wrapper = &'a mut Wrappee (and *mut _),

  • Box<Wrapper> = Box<Wrappee> (and NonNull<_>),

  • there should be no reason that it does not work also with Arc<Wrapper> = Arc<Wrappee>, but there is currently no warranty w.r.t. the transitivity of structural equivalence when one of the structs is neither #[repr(transparent)] nor #[repr(C)], and in the case of Arc / Rc a ArcBox / RcBox #[repr(Rust)] struct wraps the pointee.

Finally, unsizedness makes this more subtle, given that there is an implicit len: usize (or vtable: *const ()) field carried around during the transmutation of pointers.

Hence @vitalyd’s suggested solution.

1 Like

Thanks a lot for this in-detail explanation!