HiveBrain v1.2.0
Get Started
← Back to all entries
patternrustCritical

Why can't I store a value and a reference to that value in the same struct?

Submitted by: @import:stackoverflow-api··
0
Viewed 0 times
structwhyandsamethecanstorevaluethatreference

Problem

I have a value and I want to store that value and a reference to
something inside that value in my own type:

struct Thing {
    count: u32,
}

struct Combined(Thing, &'a u32);

fn make_combined() -> Combined {
    let thing = Thing { count: 42 };

    Combined(thing, &thing.count)
}


Sometimes, I have a value and I want to store that value and a reference to
that value in the same structure:

struct Combined(Thing, &'a Thing);

fn make_combined() -> Combined {
    let thing = Thing::new();

    Combined(thing, &thing)
}


Sometimes, I'm not even taking a reference of the value and I get the
same error:

struct Combined(Parent, Child);

fn make_combined() -> Combined {
    let parent = Parent::new();
    let child = parent.child();

    Combined(parent, child)
}


In each of these cases, I get an error that one of the values "does
not live long enough". What does this error mean?

Solution

Let's look at a simple implementation of this:

struct Parent {
    count: u32,
}

struct Child {
    parent: &'a Parent,
}

struct Combined {
    parent: Parent,
    child: Child,
}

impl Combined {
    fn new() -> Self {
        let parent = Parent { count: 42 };
        let child = Child { parent: &parent };

        Combined { parent, child }
    }
}

fn main() {}


This will fail with the error:
error[E0515]: cannot return value referencing local variable parent
--> src/main.rs:19:9
|
17 | let child = Child { parent: &parent };
| -------
parent is borrowed here
18 |
19 | Combined { parent, child }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ returns a value referencing data owned by the current function

error[E0505]: cannot move out of
parent because it is borrowed
--> src/main.rs:19:20
|
14 | impl Combined {
| -- lifetime
'a defined here
...
17 | let child = Child { parent: &parent };
| ------- borrow of
parent occurs here
18 |
19 | Combined { parent, child }
| -----------^^^^^^---------
| | |
| | move out of
parent occurs here
| returning this value requires that
parent is borrowed for 'a


To completely understand this error, you have to think about how the
values are represented in memory and what happens when you move
those values. Let's annotate Combined::new with some hypothetical
memory addresses that show where values are located:

let parent = Parent { count: 42 };
// `parent` lives at address 0x1000 and takes up 4 bytes
// The value of `parent` is 42 
let child = Child { parent: &parent };
// `child` lives at address 0x1010 and takes up 4 bytes
// The value of `child` is 0x1000
         
Combined { parent, child }
// The return value lives at address 0x2000 and takes up 8 bytes
// `parent` is moved to 0x2000
// `child` is ... ?


What should happen to child? If the value was just moved like parent
was, then it would refer to memory that no longer is guaranteed to
have a valid value in it. Any other piece of code is allowed to store
values at memory address 0x1000. Accessing that memory assuming it was
an integer could lead to crashes and/or security bugs, and is one of
the main categories of errors that Rust prevents.

This is exactly the problem that lifetimes prevent. A lifetime is a
bit of metadata that allows you and the compiler to know how long a
value will be valid at its current memory location. That's an
important distinction, as it's a common mistake Rust newcomers make.
Rust lifetimes are not the time period between when an object is
created and when it is destroyed!

As an analogy, think of it this way: During a person's life, they will
reside in many different locations, each with a distinct address. A
Rust lifetime is concerned with the address you currently reside at,
not about whenever you will die in the future (although dying also
changes your address). Every time you move it's relevant because your
address is no longer valid.

It's also important to note that lifetimes do not change your code; your
code controls the lifetimes, your lifetimes don't control the code. The
pithy saying is "lifetimes are descriptive, not prescriptive".

Let's annotate Combined::new with some line numbers which we will use
to highlight lifetimes:

{                                          // 0
    let parent = Parent { count: 42 };     // 1
    let child = Child { parent: &parent }; // 2
                                           // 3
    Combined { parent, child }             // 4
}                                          // 5


The concrete lifetime of parent is from 1 to 4, inclusive (which I'll
represent as [1,4]). The concrete lifetime of child is [2,4], and
the concrete lifetime of the return value is [4,5]. It's
possible to have concrete lifetimes that start at zero - that would
represent the lifetime of a parameter to a function or something that
existed outside of the block.

Note that the lifetime of child itself is [2,4], but that it refers
to a value with a lifetime of [1,4]. This is fine as long as the
referring value becomes invalid before the referred-to value does. The
problem occurs when we try to return child from the block. This would
"over-extend" the lifetime beyond its natural length.

This new knowledge should explain the first two examples. The third
one requires looking at the implementation of Parent::child. Chances
are, it will look something like this:

impl Parent {
    fn child(&self) -> Child { /* ... */ }
}


This uses lifetime elision to avoid writing explicit generic
lifetime parameters. It is equivalent to:

impl Parent {
    fn child(&'a self) -> Child { /* ... */ }
}


In both cases, the method says that a Child structure will be
returned that has been parameterized with the concrete lifetim

Code Snippets

struct Parent {
    count: u32,
}

struct Child<'a> {
    parent: &'a Parent,
}

struct Combined<'a> {
    parent: Parent,
    child: Child<'a>,
}

impl<'a> Combined<'a> {
    fn new() -> Self {
        let parent = Parent { count: 42 };
        let child = Child { parent: &parent };

        Combined { parent, child }
    }
}

fn main() {}
let parent = Parent { count: 42 };
// `parent` lives at address 0x1000 and takes up 4 bytes
// The value of `parent` is 42 
let child = Child { parent: &parent };
// `child` lives at address 0x1010 and takes up 4 bytes
// The value of `child` is 0x1000
         
Combined { parent, child }
// The return value lives at address 0x2000 and takes up 8 bytes
// `parent` is moved to 0x2000
// `child` is ... ?
{                                          // 0
    let parent = Parent { count: 42 };     // 1
    let child = Child { parent: &parent }; // 2
                                           // 3
    Combined { parent, child }             // 4
}                                          // 5
impl Parent {
    fn child(&self) -> Child { /* ... */ }
}
impl Parent {
    fn child<'a>(&'a self) -> Child<'a> { /* ... */ }
}

Context

Stack Overflow Q#32300132, score: 525

Revisions (0)

No revisions yet.