patternrustCritical
Why can't I store a value and a reference to that value in the same struct?
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:
Sometimes, I have a value and I want to store that value and a reference to
that value in the same structure:
Sometimes, I'm not even taking a reference of the value and I get the
same error:
In each of these cases, I get an error that one of the values "does
not live long enough". What does this error mean?
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:
This will fail with the error:
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
memory addresses that show where values are located:
What should happen to
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
to highlight lifetimes:
The concrete lifetime of
represent as
the concrete lifetime of the return value is
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
to a value with a lifetime of
referring value becomes invalid before the referred-to value does. The
problem occurs when we try to return
"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
are, it will look something like this:
This uses lifetime elision to avoid writing explicit generic
lifetime parameters. It is equivalent to:
In both cases, the method says that a
returned that has been parameterized with the concrete lifetim
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 hypotheticalmemory 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 parentwas, 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 useto highlight lifetimes:
{ // 0
let parent = Parent { count: 42 }; // 1
let child = Child { parent: &parent }; // 2
// 3
Combined { parent, child } // 4
} // 5The concrete lifetime of
parent is from 1 to 4, inclusive (which I'llrepresent as
[1,4]). The concrete lifetime of child is [2,4], andthe concrete lifetime of the return value is
[4,5]. It'spossible 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 refersto a value with a lifetime of
[1,4]. This is fine as long as thereferring 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. Chancesare, 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 bereturned 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
} // 5impl 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.