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 Tand&'a mut Tare covariant over'a; -
'x: 'aand'y: 'a, i.e.'ais a subtype of both'xand'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.