Mem::swap using one of the arguments


#1

I recently came across the situation where I wanted to replace a mutable reference with something else that contains it. See this toy example. For obvious reasons, the borrow checker does not allow this.

However, since the value of the mutable reference is immediately replaced with something else, much like mem::swap does, I think there should be a safe way to do this. I was hoping I could overcome this by using either (or a combination of) mem::replace and mem::swap so as not to litter the code with unsafe blocks. I could not find a way, however. I ended up implementing the following helper function:

fn swap_using<T, F>(x: &mut T, f: F)
    where F: FnOnce(T) -> T
{
    unsafe {
        let y = mem::replace(x, mem::uninitialized());
        *x = f(y);
    }
}

The toy example can then cleanly be written like this.

I wonder

  1. if there was another way of solving this problem without changing the struct or passing by value;
  2. whether this helper function (regardless of its implementation) would make a useful addition to the core::mem module.

#2

This is basically the same as the closed RFC for mem::replace_with. The biggest challenge is what to do when that function panics.

Beware, if your type implements Drop, this assignment to *x will call x.drop() first – on your uninitialized value! This is why the RFC used ptr::write to just clobber the existing contents.


#3

Thank you very much for the clarification!

I was a bit disappointed to hear that the RFC got closed (or at least postponed). It was quite an interesting read. I guess I can use the take_mut crate that the RFC refers to, for now.

I am still curious how other programmers solve this.

  1. By using a different data structure? (e.g. using Option and Option.take)
  2. By using two mem::replaces with a nonsense/default value?
  3. By using the aforementioned crate or by implementing a something helper function?
  4. By using unsafe code?
  5. Something I haven’t thought about?

#4

One thought: since you’re already committed to moving self into newly-allocated memory, why not let Rust do it?


#5

Consuming self is great if that fits your usage model. But I think that only helps here if you’re only inserting at the root of the tree, otherwise you’d have the same problem about moving out of parent nodes.

Using Option is not too bad, or in this toy example I might add an Empty variant to your enum Tree. You might want to have the possibility of an empty root anyway, right? Then insert is just:

impl<T> Tree<T> {
    pub fn insert(&mut self, item: T) {
        let x = std::mem::replace(self, Tree::Empty);
        *self = Tree::Branch(Box::new(x), Box::new(Tree::Leaf(item)))
    }
}

Or since you probably want to normalize Empty away, more like:

impl<T> Tree<T> {
    pub fn insert(&mut self, item: T) {
        if let Tree::Empty = *self {
            *self = Tree::Leaf(item);
        } else {
            let x = std::mem::replace(self, Tree::Empty);
            *self = Tree::Branch(Box::new(x), Box::new(Tree::Leaf(item)));
        }
    }
}

#6

Thank you for sharing your thoughts!

@derspiny: Yeah, I think I agree with @cuviper that passing by value doesn’t seem to fit here because it is only changing a small part the tree. (Or maybe I should re-evaluate my views which are probably biased by other languages. Feel free to tell me!)

@cuviper: When looking at other implementations of trees in Rust, they were indeed almost always using Option. Your alternative also totally makes sense in this toy example. The kind of tree where this problem arose, is a sort of “partition tree”. I modelled it like this:

struct PartitionTree<'a, T: 'a, D: 'a> {
    root: Option<Node<'a, T, D>>,
    scope: Option<Range<T>>,
}

enum Node<'a, T: 'a, D: 'a> {
    Leaf { start: T, end: T, data: &'a D },
    Branch {
        left: Box<Node<'a, T, D>>,
        right: Box<Node<'a, T, D>>,
        free: Range<T>,
    },
}

I don’t know if this structure makes sense. Feel free to share your thoughts. The cool thing about it, is that by not using empty variants, you can simplify the code handling it, because you only have to consider two cases (a leaf or a branch) instead of four cases (a branch with no children (empty leafs), branch with a left child, a branch with a right child and a branch with two children).


#7

OK, but this model still doesn’t have a way to represent temporary node absence, right? So you can either add an Empty variant, or make the branch children Option<Box<Node>>, but either way the cases you have to consider are about the same. Or you can very carefully dip into unsafe code to move stuff.

The good news is that both safe options are free in memory size, as Empty is just another discriminant value, and Option<Box<_>> uses the fact that Box's pointer is never null to represent None vs Some. So the only cost is having to deal with possibly-absent nodes everywhere, even when this is only supposed to happen temporarily in isolated cases.


#8

@cuviper: You’re right. However, checking everywhere for nodes that are None or Empty which can never happen except at the root, was what I was trying to avoid by structuring it like this.

Maybe a combination of what @derspiny and you suggest might be the best of both worlds? See this example. The wrapper struct with Option allows ownership on the inner struct, while still providing the same external interface. I think I am happy with this.

What do you think?


#9

I think that only lets you insert at the root, and other tree operations like balance and remove will be difficult. But try it and see how it goes! :slight_smile:


#10

This is what I have so far for the partition tree I mentioned earlier. Only the insert operation is implemented at the moment. The version using take_mut looks a lot cleaner and shorter though because it doesn’t involve reconstructing and passing self around. Yeah, I guess we’ll see how the balancing, removal and iteration goes.

Thank you for the interesting conversation!