As Rust implies move semantics by default and quite strict borrowing rules, often, there are situations (especially, with large struct
s and enum
s) where mutating value in-place or values swapping may not be allowed by borrow checker, which is quite confusing and leads to doing needless clones (so providing redudant performance costs). For example:
impl<T> Buffer<T> {
fn get_and_reset(&mut self) -> Vec<T> {
// error: cannot move out of dereference of `&mut`-pointer
let buf = self.buf;
self.buf = Vec::new();
buf
}
}
A neat and need-to-know trick in such situations is to use mem::replace
(or mem::swap
). It allows to swap two values of the same type without moving things around, partial destructuring and references mess. So, the example above is simply turns into:
impl<T> Buffer<T> {
fn get_and_reset(&mut self) -> Vec<T> {
mem::replace(&mut self.buf, Vec::new())
}
}
For better understanding mem::replace
, mem::swap
and mem::take
purpose, design, limitations and use cases, read through the following articles:
- Official
mem::replace
docs - Official
mem::swap
docs - Official
mem::take
docs - Karol Kuczmarski: Moving out of a container in Rust
- Ferrous Systems: Using
mem::take
to reduce heap allocations
Some examples of useful applying these functions are described below.
This situation has detailed explanation in the following article:
The borrow checker won't allow us to take out
name
of the enum (because something must be there). We could of course.clone()
name and put the clone into ourMyEnum::B
, but that would be an instance of the "Clone to satisfy the borrow checker" antipattern. Anyway, we can avoid the extra allocation by changinge
with only a mutable borrow.
mem::replace
lets us swap out the value, replacing it with something else. In this case, we put in an emptyString
, which does not need to allocate. As a result, we get the originalname
as an owned value. We can then wrap this in another enum.
enum MyEnum {
A { name: String },
B { name: String },
}
fn swizzle(e: &mut MyEnum) {
use self::MyEnum::*;
*e = match *e {
// Ownership rules do not allow taking `name` by value, but we cannot
// take the value out of a mutable reference, unless we replace it:
A { ref mut name } => B { name: mem::replace(name, String::new()) },
B { ref mut name } => A { name: mem::replace(name, String::new()) },
}
}
Look ma, no allocation! Also you may feel like Indiana Jones while doing it.
Consider the following situation:
struct Names {
exclusions: Vec<String>,
names: HashSet<String>,
}
impl Names {
fn apply_exclusions(&mut self) {
self.exclusions.drain(..).for_each(|name| {
self.remove_name(&name);
})
}
fn remove_name(&mut self, name: &str) {
self.names.remove(name);
}
}
which does not compile due to 2 mutable borrows:
error[E0500]: closure requires unique access to `*self` but it is already borrowed
--> src/lib.rs:10:44
|
10 | self.exclusions.drain(..).for_each(|name| {
| ------------------------- -------- ^^^^^^ closure construction occurs here
| | |
| | first borrow later used by call
| borrow occurs here
11 | self.remove_name(&name);
| ---- second borrow occurs due to use of `*self` in closure
Using mem::take
here allows us to avoid the problem with 2 mutable borrows at almost no cost (Vec::defaukt()
is no-op), by swapping out the value in a temporary variable:
impl Names {
fn apply_exclusions(&mut self) {
let mut exclusions = mem::take(&mut self.exclusions);
exclusions.drain(..).for_each(|name| {
self.remove_name(&name);
});
}
fn remove_name(&mut self, name: &str) {
self.names.remove(name);
}
}
It's worth mentioning, that this problem became much less common after disjoint capture in closures had been introduced in 2021 Rust edition. For illustration, the self.name
mutation is intentionally separated into its own method, so we can lock the whole &mut self
. If we simplify the code straightforwardly, it just compiles fine, due to mutable borrows are disjoint:
struct Names {
exclusions: Vec<String>,
names: HashSet<String>,
}
impl Names {
fn apply_exclusions(&mut self) {
self.exclusions.drain(..).for_each(|name| {
self.names.remove(&name);
})
}
}
Estimated time: 1 day
Improve and optimize the code contained in this task's crate to cut off redudant performance costs. Add tests.
After completing everything above, you should be able to answer (and understand why) the following questions:
- What is the reason of
mem::replace
existing in Rust? What does it give to us? Why cannot we solve the same problems without it? - Provide some meaningful examples of using
mem::replace
in Rust.