Some months ago I've read the following comment by steffahn and I think a generalized &mut T
could be viable.
So here's my very first Pre-RFC based on that idea:
Summary
Generalize &mut
with a second, optional lifetime to allow opt-in downgradeable borrows.
Motivation
Currently coercing or reborrowing a &mut
as an immutable reference always extends the unique borrow to the lifetime of the resulting reference.
When interior mutability is involved, this behavior is neccessary for correctness (e.g. for Cell::as_mut
and Mutex::as_mut
to be sound),
but in other cases relaxing the borrow to a shared one might be perfectly safe.
This proposal allows get-or-insert functions and similar patterns that need an unique borrow for a short time inside the function body but want to return an immutable, shared reference.
Guide-level explanation
With this proposal, mutable references can have separately tracked "borrow" and "uniqueness" lifetimes.
If not further restricted, unique borrows created from mutable references can downgrade to shared borrows if they are only used immutably after some point:
fn main() {
let mut x: i32 = 42;
let a = &mut x; // type inferred as &'_ mut<'_> i32
a += 1;
// `a` is only coerced/reborrowed immutably after this point, so
// it can be downgraded and only the uniqueness lifetime ends here.
let b = &x;
println!("{}", a == b);
}
The separate tracking is opt-in outside function bodies, e.g. in function signatures. The borrows from mutable references stay unique, even if the references are coerced to shared references. Only if a second lifetime parameter is added (e.g. &'ref mut<'uniq>
) they can be downgraded.
E.g. the following code fails:
#[derive(Default)]
struct Cache(std::collections::HashMap<u64, String>);
impl Cache {
fn get(&self, key: u64) -> Option<&str> {
self.0.get(key)
}
fn get_or_insert(&mut self, key: u64, default: impl FnOnce() -> String) -> &str {
self.0.entry(key).or_insert_with(default)
}
}
fn main() {
let mut cache = Cache::default();
let a = cache.get_or_insert(1, || "foo".into());
let b = cache.get(1);
// ^ Error: cannot borrow `cache` as immutable because it is also borrowed as mutable
println!("{a}, {b}");
}
With downgradeable borrows, the code can compile by adding the second lifetime parameter and specifying that the return value of get_or_insert
only uses the borrow lifetime:
fn get_or_insert<'a>(&'a mut<'_> self, key: u64, default: impl FnOnce() -> String) -> &'a str {
self.0.entry(key).or_insert_with(default)
}
Where it's neccessary for soundness, the borrows can't be downgraded, as the function signatures of e.g. Cell::get_mut()
won't allow downgrading of the returned borrow:
fn main() {
let mut x: Cell<Option<i32>> = Cell::new(Some(42));
let a: &i32 = x.get_mut().as_ref().unwrap();
x.replace(None);
// ^ Error: cannot borrow `x` as immutable because it is also borrowed as mutable
println!("{}", a);
}
Reference-level explanation
Mutable reference types &mut T
have two associated lifetimes. One describing the total duration of the borrow and one describing the duration the borrow needs to be unique: &'ref mut<'uniq> T
with the implicit constraint 'ref: 'uniq
.
The uniqueness lifetime can be omitted and is then forced to be equal to the borrow lifetime. &'a mut T
is thus short for &'a mut<'a> T
.
Such references with both lifetimes equal can be coerced to mutable references with shorter lifetime only if both lifetimes stay equal.
Besides this restriction, mutable references &'a_ref mut<'a_mut> T
can be coerced to &'b_ref mut<'b_mut> T
, as long as 'a_ref: 'b_ref, 'a_mut: 'b_mut
holds,
and all mutable references &'a_ref mut<'a_mut> T
can be coerced to &'b T
, as long as 'a_ref: 'b
holds.
Coercing to or reborrowing as a shared reference doesn't extend the uniqueness lifetime.
For backwards compatibility &mut T
in function signatures desugars to &'_a mut<'_a> T
. This ensures that functions like Cell::get_mut
stay sound.
This makes the feature opt-in for all interfaces.
Drawbacks
- Introducing a built-in type with two lifetimes makes the language more complex and harder to learn.
Rationale and alternatives
- Do nothing and continue to only allow such patterns with some kind of interior mutability.
- An implementation of partial borrows could also include downgradeable borrows as a special case of the partial borrowing rules.
Prior art
TBD
Unresolved questions
- Is there a better syntax than
&'a mut<'b> T
? Maybe&'a mut 'b T
? - Type inference might need some tweaks to match e.g.
&'a mut<'b> T
to animpl<'a> Trait for &'a mut T
. - Unless
Entry
,DerefMut
, etc. are modified to allow the second lifetime, most code won't be able to take advantage of the feature. It's hard to say if this is even possible for all types and traits for which downgradeable borrows would be helpful without a breaking change.
Future possibilities
In a future edition, the desugaring of &mut T
in function arguments might be relaxed to a
downgradeable reference with two anonymous lifetimes. This would allow more code to compile,
but could make functions with interior mutability through unsafe code unsound, if those that
need it forget to constrain the argument to a single lifetime. So that would need a detailed
transition plan.
`