Skip to content

Latest commit

 

History

History
 
 

2_2_mem_replace

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

Task 2.2: Swapping values with mem::replace

As Rust implies move semantics by default and quite strict borrowing rules, often, there are situations (especially, with large structs and enums) 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:

Some examples of useful applying these functions are described below.

Keeping owned values in changed enums

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 our MyEnum::B, but that would be an instance of the "Clone to satisfy the borrow checker" antipattern. Anyway, we can avoid the extra allocation by changing e 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 empty String, which does not need to allocate. As a result, we get the original name 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.

Mutating embedded collection

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);
        })
    }
}

Task

Estimated time: 1 day

Improve and optimize the code contained in this task's crate to cut off redudant performance costs. Add tests.

Questions

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.