Avoiding self-referential struct leaks implementation details

I have a collection of images, and an associated collection of views into the former.

Here is some outline code which summarizes the situation:

struct Image;

struct SubImage<'i> {
    image: &'i Image,
    metadata: u32,
}

fn foo() {
    let full_images: Vec<Image> = vec![Image, Image, Image];
    let sub_images: Vec<SubImage<'_>> = vec![ 
        SubImage { image: &full_images[0], metadata: 42  }
        SubImage { image: &full_images[2], metadata: 666 }
    ];
}

This is fine as long as sub_images does not outlive full_images, and it's trivial for this to work if these two variables live in a function's local scope.

However, I want a set of such images and sub-images to be created in some producer function and returned for use elsewhere. So I'm looking for a way to package them up together, which naively leads to something like this

struct ImageSet {
    full_images: Vec<Image>,
    sub_images: Vec<SubImage<'_>>,
}

which is a self-referential struct (a no-go in Rust), because elements of sub_images refer to elements of full_images via the ref in the SubImage struct.

An obvious idea to eliminate the self-referential nature of the ImageSet struct, is to turn SubImage::image from a ref into an index. However, previously SubImage had some methods, roughly like

impl SubImage<'_> {
    fn render(&self) { ... }
    fn tweak_metadata(&mut self) { ... }
}

which enabled the client to treat each sub-image as a self-contained item. Replacing the ref with an index would burden the client with manually maintaining the association between the full images and the sub-images: the original SubImage knew exactly which Image it referred to; the new version knows that it refers to the Nth image in some unknown vector of Images.

Can you suggest an approach where the client can use the sub-images without having to care about which full images they refer to?

1 Like

If you define it like this:

struct SubImage<I> {
    image: I,
    metadata: u32,
}

impl<I:Borrow<Image>> SubImage<I> { … }

then SubImage<&Image> will work as before, but you also can use SubImage<Image>, SubImage<Rc<Image>>, or similar to avoid the lifetime encumbrance.

4 Likes

This is a self-referential struct, and it's impossible in Rust. You can't have a struct that borrows any data that exists in the same struct. Rust's types can only borrow data they don't have.

You could wrap Image in Rc or Arc and then use that in SubImage. Or you need to restructure the program to obtain Image first, then borrow it. You can have:

ImageSet<'i> { image: &'i Image, sub_image: SubImage<'i> }

If you just want to move around who cares, you can have a

pub struct SubImageToken { /* priv */ id: SomeKey }
pub struct SubImage<'i> { set: &'i ImageSet, image: &'i Image }

impl ImageSet {
    fn sub_image(&self, SubImageToken) -> SubImage<'_>
}

With optional methods on SubImage<'_> that call back to the ImageSet. It does open up the possibility of creating a SubImage<'_> from set A with a token from set B, which I guess is part of your complaint.

One way to address that is to have unique keys and fallible lookup.

Another way to guarantee it are approaches like GhostCell where you use a callback pattern and your tokens would also be borrows.

And another way is what was already mentioned: use shared ownership (Rc<_>, etc) and only create SubImages that shared-own their source set.

1 Like

Exactly.

Indeed, this is a pretty obvious approach which is easy to implement and reason about, and where I don't see any significant downsides.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.