Understanding Rust lifetime and mutability
Lifetime and mutability are simple concepts. However, when combined with reborrow and subtyping, it could get very confusing. Here's a summary of my current understandings.
Basic rules
Immutable rule: Within the lifetime of an immutable reference to a variable, the variable can only be used as immutable references.
Examples:
struct S{}
fn main() {
let mut x = S{};
let rx = &x;
&x; // can have another immutable reference
// x = S{}; // cannot assign to x
// let y = x; // cannot move
// x; // cannot implicit move
// let mrx = &mut x; // cannot have mutable reference to x
rx;
}
Note that if struct S
implements the Copy
trait, then x
can be
copied, because Copy
uses Clone
, and Clone::clone(&self)
takes
an immutable reference.
The following is an example with an explicit lifetime parameter:
fn foo<'a>(x: &'a u32) -> &'a u32 {
&1
}
fn main() {
let mut x = 1;
let ry = foo(&x);
// x = 2; // cannot assign to x
ry;
}
In the above example, even though the value of x
is not related to
ry
, the function call foo(&x)
creates a immutable reference of x
which shares the same lifetime 'a
with reference ry
.
Mutable rule: A mutable reference is equivalent to a temporary move.
Examples:
fn main() {
let mut x = 1;
let rx = &x;
let mrx = &mut x; // x is temporarily moved to *mrx
// x; // cannot use x because it is moved to mrx
// rx; // same for rx
mrx; // move ends after this line
x; // can use x again after move ends
}
Reborrow
Mutable reference &'a mut T
does not implement the Copy
trait.
There are 2 ways of accessing mutable references: move and reborrow.
A move example:
fn main() {
let x = &mut 1;
let y = x; // move x to y
// x; // cannot use x
}
Reborrow is implicitly accessing a mutable reference x
as &*x
or
&mut *x
. &*x
is the same as immutable reference, and &mut *x
is
the same as mutable reference. The following shows examples of
reborrow:
fn foo(x: &mut u32) {
*x = 2;
}
fn main() {
let x = &mut 1;
foo(x); // reborrow for function argument
let y: &u32 = x; // reborrow: y = &*x
let z: &mut u32 = x; // reborrow: z = &mut *x
x; // can use x again after temp move ends
}
Reborrow happens when a mutable reference's type changes. In the
above example: let z: &mut u32 = x
changes type of x
from &'x mut u32
to &'z mut u32
. The type change is not necessarily
"weakening". For example, the assignment in the above example: let y: &u32 = x
, changes the type of x
from a mutable reference to an
immutable reference. Immutable references are not supertypes of
mutable references (&T
implements Copy
but &mut T
does not).
It is worth noting that the following identity function id()
moves
mutable references instead of reborrow:
fn id<T>(x: T) -> T {
x
}
fn main() {
let x = &mut 1;
id(x); // x is moved into id()
// x; // cannot use x
}
More info about the identity function can be found in this blog post.
Subtyping
Subtyping "duplicates" a variable with a weaker type. It happens to all assignments and function arguments when the target type is "weakened". Subtyping follows the variance rules.
An example of lifetime subtyping:
fn foo<'a>(x: &'a mut u32, y: &'a u32) {}
fn main() {
let x = &mut 1;
let y = &2;
foo(x, y);
x;
y;
}
In the above example, what is the region covered by the lifetime
parameter 'a
?
When calling foo(x, y)
, rust subtypes function arguments x
and
y
. The type of x
is changed from &'x mut u32
to &'a mut u32
,
and the type of y
is changed from &'y u32
to &'a u32
. The
subtyping is good given that:
-
&'a T
and&'a mut T
are covariant over'a
; -
'x: 'a
and'y: 'a
, i.e.'a
is a subtype of both'x
and'y
.
The region of 'a
could be as small as possible. The minimum region
of 'a
contains the single line foo(x, y)
.
Note that in foo(x, y)
, subtyping happens for both reborrow (x
)
and copy (y
).
Examples
Now we have all the basic concepts. Let's apply them to some examples.
Example 1
fn get_x<'a, 'b>(x: &'b &'a mut u32) -> &'a u32 {
*x
}
This above function does not compile. However, with trivial fixes, the following 2 functions compiles:
fn get_x1<'a, 'b>(x: &'b &'a mut u32) -> &'b u32 {
*x
}
fn get_x2<'a, 'b>(x: &'b &'a u32) -> &'a u32 {
*x
}
A mutable reference is equivalent to a temporary move. So for &'b &'a mut u32
, the value is moved to x
for lifetime 'b
, and we cannot
borrow the value for 'a
where'a: 'b
. That's why get_x()
does
not compile but get_x1()
does.
For immutable reference &'b &'a u32
the value is not moved, so
get_x2()
is good.
Example 2
fn foo<'a, 'b>(x: &'b mut &'a u32, y: &'a mut u32) {}
fn main() {
let mut x = &1;
let mut y = 2;
foo(&mut x, &mut y);
y;
x;
}
The above code does not compile. To understand why, we need to find
out the region of lifetime 'a
.
Let's first de-sugar the main()
function:
fn main() {
let mut x = &1;
let mut y = 2;
let rx = &mut x;
let ry = &mut y;
foo(rx, ry);
y;
x;
}
When calling function foo()
, subtypes of rx
and ry
are created:
rx: &'rx mut &'x u32 --> &'b mut &'a u32
ry: &'ry mut u32 --> &'a mut u32
By the variance
rules,
&'a mut T
is invariant over T
. For &'rx mut &'x u32
, T
is
&'x u32
. The invariant property requires &'x u32 == &'a u32
, so
'a = 'x
.
The subtype for ry
takes a mutable reference of y
for lifetime
'a
, so y
is not accessible within 'a
. Thus the code does not
compile.