Consider the following code:
struct VecWrapper<'a, T>(&'a mut Vec<T>);
impl<'a> VecWrapper<'a> {
pub fn new(vec: &'a mut Vec<T>) -> Self { VecWrapper(vec) }
pub fn something(&self){ ... }
}
let mut vec = vec![1];
let vec_wrap = VecWrapper::new(&mut vec);
vec_wrap.something();
vec.push(1);
vec_wrap.something();
This code does not compile, because push
requires a mutable reference at the same time vec_wrap
has one.
However, there is no inherent reason this wouldn't work; it should, in theory, be equivalent to
let mut vec = vec![1];
VecWrapper::new(&mut vec).something();
vec.push(1);
VecWrapper::new(&mut vec).something();
Which compiles and runs sucessfully.
Why, then, is this pattern not this allowed?
To answer this, consider this code:
struct VecWrapper2<'a, T>(&'a mut T);
impl<'a> VecWrapper2<'a> {
pub fn new(vec: &'a mut Vec<T>) -> Self { VecWrapper2(&mut vec[0]) }
pub fn something(&self){ ... }
}
let mut vec = vec![1];
let vec_wrap = VecWrapper2::new(&mut vec);
vec_wrap.something();
vec.push(1); // reallocates the array
vec_wrap.something(); // use after free
Here, this is clearly not ok -- and the compiler informs us of this with a compile error (in fact, the same error that blocked the first example).
However, it would be useful for a number of things if the original code compiled.
To address this, we'll consider a new language feature: 'noncontinuous lifetime parameters'. Noncontinuous lifetime parameters are defined with the following (placeholder) syntax:
// v non continuous lifetime parameter
struct VecWrapper<''a, T>(&''a mut Vec<T>);
// ^ usage
Noncontinuous lifetimes, as the name implies, do not have to be continuous -- they can have breaks in them.
Noncontinuous lifetime parameters must also obey these additional rules:
- Noncontinuous lifetime parameters are not assignable to continuous lifetime parameters (though continuous lifetime parameters are assignable to noncontinuous lifetime parameters)
- Noncontinuous lifetime parameters are not preserved in refutable patterns
We'll now edit the previous example to use noncontinuous lifetime parameters:
struct VecWrapper<''a, T>(&''a mut Vec<T>);
impl<''a> VecWrapper<''a> {
pub fn new(vec: &''a mut Vec<T>) -> Self { VecWrapper(&mut vec) }
pub fn something(&self){ ... }
}
let vec = vec![1];
let vec_wrap = VecWrapper::new(&mut vec);
vec_wrap.something();
vec.push(1);
vec_wrap.something();
Unlike before, there are never two simultaneous mutable references, because the
lifetime parameter in VecWrapper
only encompasses the two .something()
calls
-- there's a gap around the .push()
. Thus, this code compiles successfully.
On the other hand, if we try to use noncontinuous lifetime parameters in
VecWrapper2
, we'll get a compile error:
struct VecWrapper2<''a, T>(&''a mut T); // ok
impl<''a> VecWrapper2<''a> {
pub fn new(vec: &''a mut Vec<T>) { VecWrapper2(&mut vec[0]) } // compile error
pub fn something(&self){ ... }
}
This errors because of rule #1 -- the noncontinuous lifetime parameter ''a
isn't assignable to the continuous lifetime parameter defined in the IndexMut
trait.
Rules #1 and #2 make this feature safe. There are 4 primitive operations to manipulate references:
- Accessing struct properties or pattern matching on structs
- Pattern matching on enums
- Accessing union variants (unsafe)
- Doing stuff with raw pointers (unsafe)
For the first one, it's ok to preserve a noncontinuous lifetime, as the struct field will always exist.
For the second, it is not ok to preserve a noncontinuous lifetime, as variant fields can be invalidated if the enum value is changed to a different variant. This is covered by rule #2.
The last two are already unsafe, so it's up to the implementor to determine if they can safely preserve noncontinuous lifetimes. These are thus covered by rule #1. If the implementor determines that it is safe to preserve a noncontinuous lifetime, they use a noncontinuous lifetime parameter. Otherwise, they should leave it as a continuous lifetime parameter, and the compiler knows to statically check that a noncontinuous lifetime is not assigned to it.
This feature would allow for many new patterns to compile. For example, consider the following code:
let mut val = 0;
let inc = || val += 1;
let dec = || val -= 1;
inc();
inc();
dec();
inc();
This is currently a compile error, as inc
and dec
need to share a mutable
reference to val
.
If noncontinuous lifetimes were added, closures could always borrow their
captures noncontinuously, allowing the above code to compile. inc
's capture's
lifetime would span its definition and the three calls to it, with a gap around
dec
's definition and its singular call. dec
's capture's lifetime would span
its definition and its call, and have a gap around the first two inc
calls.