Simple question about borrowing and references

Howdy all! I've been writing Rust for about 2 weeks and LOVING IT. I've read through the book and other things and have gotten through about 5k lines of code that are working with tests and all.

I'm running into a borrowing problem. I understand what's happening, and why it's failing, but I'm not sure the best approach to fix it. I'd bet its a larger design issue.

Basically, in a loop that's in a function, I build up a custom struct that requires a reference. I then add that type to a vector. But, when the loop goes out of scope, I lose a value that has been borrowed. Crash.

Here is a stripped down portion of the code in question. This produces the same error as my full (and convoluted) code.

struct Footprint<'a> {
    points: &'a Vec<Point>,
    angles: Angle<'a>,
    lines: Line<'a>,
}

impl<'a> Footprint<'a> {
    pub fn from(points: &Vec<Point>) -> Footprint {
        Footprint {
            points,
            lines: Line {points: &points},
            angles: Angle {points: &points},
        }
    }
}

struct Line<'a> {
    points: &'a Vec<Point>
}

struct Angle<'a> {
    points: &'a Vec<Point>
}

struct Point {
    x: f32,
    y: f32,
}

fn main() {
    let given: Vec<Vec<f32>> = vec![
        vec![0.0, 2.0],
        vec![1.0, 3.0],
        vec![4.0, 0.0],
    ];

    let built = make_footprints(given);
}

fn make_footprints<'a>(coords: Vec<Vec<f32>>) -> Vec<Footprint<'a>> {
    let mut points = vec![];
    for coord in coords {
        let point = Point {
            x: coord[0],
            y: coord[1],
        };

        points.push(point);
    }

    let footprint = Footprint::from(&points);
    vec![footprint]
}

And the pertinent error is:
error[E0597]: points does not live long enough
--> src/main.rs:37:38
|
37 | let footprint = Footprint::from(&points);
| ^^^^^^ borrowed value does not live long enough
38 | vec![footprint]
39 | }
| - borrowed value only lives until here
|
note: borrowed value must be valid for the lifetime 'a as defined on the function body at 26:20...

I get that the issue is I am creating points points inside make_footprints(), and then handing the reference to points into Footprint, which needs to live longer than the original points. I feel one way to solve this would be for Footprint not to take a reference, but for other reasons, I think it has to.

I have also played around with collect() and map(), but ran into the same issue.

What would be great is to move that reference into footprint, but I'm not sure how to do that.

I am sure I am misunderstanding and butchering the borrow checker. Can anyone provide some guidance? Thank you!

The first point is correct; but the "outlives" relationship is the other way: the source of the reference in Footprint, i.e. points, has to outlive the reference.

"Moving a reference" in that sense is not possible. References themselves are moved all the time, but that doesn't touch the referred-to object.

It would be interesting to know why you need the reference in Footprint. With only the code given here, it would be the most logical thing to make the footprint own the vector of points.

If there are cases where the vector is owned, and cases where it is borrowed, you can use a std::borrow::Cow that handles both cases.

1 Like

@birkenfeld thanks for the quick response. And, I get what you mean by "outlives".

I guess I'm not 100% sure that Footprint needs to borrow points, but in the full code, Footprint is made up of several Triangles which are made up of Lines and Angles which are made up of Points. Right now, all of those pieces borrow the same points. They are all immutable, so it's working so far.

Does that sound like an instance where each should own? I didn't think it was possible for multiple variables to own the same reference (that's the whole point, right?).

I have updated my original code to show more. Specifically, that Footprint has Angle, and Line, all of which share a reference to the Point vector.

I mean, you certainly can use references in Footprint, but then a function like make_footprint has to be written differently.

  • Like written, points is dropped at the end of the function, and the reference becomes dangling. This is obviously forbidden.
  • Now you might think it would be possible to move the points out together with the reference, i.e. something like return (points, vec![footprint]). This is less obviously bad, but consider that the reference within footprint is pointing to the location of points on the function's stack. When returning from the function, the object is moved into the caller's stack frame, again letting the reference dangle.

In most cases, functions returning something with a lifetime 'a without a corresponding input argument also containing 'a are impossible. There are exceptions, but they are either in unsafe code or in edge cases like fn empty<'a>() -> &'a [u32] { &[] }

So in this case, the vector of points has to be higher up the "lifetime stack" (usually that means call stack), in order to have a stable address which references can point at.

If you mean "own the same object" ("owning a reference" is confusing for Rust ears), it is possible with a wrapper type like Rc<T> that implements shared ownership with immutable access. For more complicated object relationships, Rc and its complementing type for mutable access, RefCell, are often used.

1 Like

As an aside, this is an unusual type to use in this kind of code. You can always borrow the contents of a vector as a slice, and if you're storing shared references to a Vec, you won't be able to mutate it anyway, so you get rid of a layer of indirection by using the slice directly. (So instead of using &'a Vec<Point> you should probably be using &'a [Point] instead.)

2 Likes

If Footprint is what owns the Point values, then you can perhaps go with an impl like this:

struct Footprint {
    points: Vec<Point>,
}

impl Footprint {
    pub fn from(points: Vec<Point>) -> Footprint {
        Footprint { points }
    }

    pub fn angles(&self) -> Angle<'_> {
        Angle {
            points: &self.points,
        }
    }

    pub fn lines(&self) -> Line<'_> {
        Line {
            points: &self.points,
        }
    }
}

struct Line<'a> {
    points: &'a [Point],
}

struct Angle<'a> {
    points: &'a [Point],
}

struct Point {
    x: f32,
    y: f32,
}

fn main() {
    let given: Vec<Vec<f32>> = vec![vec![0.0, 2.0], vec![1.0, 3.0], vec![4.0, 0.0]];

    let built = make_footprints(given);
}

fn make_footprints(coords: Vec<Vec<f32>>) -> Vec<Footprint> {
    let mut points = vec![];
    for coord in coords {
        let point = Point {
            x: coord[0],
            y: coord[1],
        };

        points.push(point);
    }

    let footprint = Footprint::from(points);
    vec![footprint]
}

You can't have a Vec<Point> inside Footprint and also reference that Vec from Line and Angle stored in there as well - that would create a self-referential struct, which is (for all intents and purposes) disallowed by Rust. So if you want/need Footprint and the stored Line and Angle to all hold references to the same points, the reference needs to come from outside (e.g. make_footprints() should take a reference to those points).

You can also use Rc as has been noted upthread, but given Point is small, why not just copy them around? If you don't need to mutate a point and observe the mutation from multiple referrers (you said it's all immutable, so should be fine), then this ought to work better than putting them behind an Rc or storing references (and dealing with resulting lifetime parameters).

If none of the suggestions thus far are suitable, then try explaining the data flow a bit more, who owns what, and how you plan on using these types - we can come up with a better design.

1 Like

Wow, I am really impressed with this community. I think I am thinking about references incorrectly. There probably isn't a solid reason why the Point vector needs to be shared. They are small, and I don't mind copying them. When I started, I was running into errors with that, but I don't even remember what they were. I'm going to try and remove those references code-wide and see what happens.

@skysch, I've already implemented your suggest (thank you IDE), and everything passes (tests and compile). Would you mind detailing or pointing me to where in the book I can go to understand why that works?

1 Like

The Deref Trait Allows Access to the Data Through a Reference - The Rust Programming Language. Vec<T> implements Deref<Target=[T]>, so you can get a slice out of it and use deref coercions to make that automatic (in places where deref coercion is done automatically).

@vitalyd, thank you. That's helpful.

And thanks to everyone. I went through and removed all those borrows and just copied Point where it needed to be. It took some doing, but

  • I was able to lose a lot of lifetime specifiers
  • Everything is much more readable.

However, I found that I had to clone() a lot, and I'm not sure if that's because I was misunderstanding something. Now that all my tests are passing, I'm going to look through it and make sure I understand why everything is as it is.

Thanks again -- this would have taken me days to figure out on my own.

1 Like

So just to make sure we're on the same page, I was referring to copying Points around, not necessarily Vecs of them. If you're finding yourself cloning Point containers over and over, then something is off and you'll be bleeding out performance. If you have immutable containers of them that you'd like to reference in a bunch of places, then consider storing them inside an Rc - so this would be an Rc<[Point]>, a reference counted slice pointer of Point values.

You may want to think carefully about the data flow in your app, who needs to own what data, what short-lived/transient structs can borrow data (temporarily) from longer-lived structs, and so on. The data flow will steer you in the right direction in terms of arranging how the data is managed.

1 Like

You're welcome - apart from it just really being a great community, we know that the beginning can be unexpectedly tough, and since we've all had our share of fights with the borrow checker we try to ease that part for newcomers especially :slight_smile:

2 Likes