Can multiple mutable references co-exist between parent and child structs?

Please consider this code:

    let mut parent = Parent { value: 0, child: Child { value: 0, } };

    let parent_mut = &mut parent;
    parent_mut.value = 1;
    let child_mut = &mut parent_mut.child; 
    child_mut.value = 2 + parent_mut.value;

The code seems to allow multiple mutable references to parent, albeit one is through the parent's child field. I'm a bit confused by this. For instance, I can think about these as being two independent references, one is to a &mut Parent, and the other to a &mut Child. But if I acquire the child reference like so:

    let child_mut = &mut parent.child; // not parent_mut.child

...the compiler will give an error about two mutable borrows
So it seems the original code maintains a link between the two mutable references and allows them to co-exist. Is it appropriate to think of them as two allowable references to Parent, or one for Parent and one for Child? If they are indeed independent references then it feels like the compiler should allow functions to be written like so:

pub fn child_set_value(child: &mut Child, parent: &mut Parent) {
    child.value = 3;
    parent.value = 3;
}

...but invoking such a function is impossible as it means duplicating borrows:

child_set_value(child_mut, parent_mut);

child_mut and parent_mut are allowed to co-exist, but at the moment of passing them to a function, new independent borrows take place and there is no longer this link between the two references. Is this the right way to think about it?

I assume the common workaround to allow the references to exist inside a function would be to use RefCell, but I haven't yet researched RefCell enough to be able to convert this example. For now I just wanted clarification that the two references can co-exist, just not within a function.

Thank you very much for your comments!

This is my full example:
Rust Playground Link

There's a few things to be aware of.

1. Borrows don't have to last as long as the scope they're in

That used to be the case about 5 years ago, but not any more. The feature is called NLL (non-lexical lifetimes). You may have already known this one.

2. You can reborrow a &mut without invalidating the &mut

That's why this works, even though &mut aren't copy and you passed it to push:

fn my_push(v: &mut Vec<String>) {
    v.push(String::new());
    println!("{v:?}");
}

An automatic reborrow of v happens when you call .push. You can't use the original v while that reborrow is still usable, but you can use v again afterwards. In this case, the reborrow ends immediately after the push.

An explicit reborrow would look like &mut *v.

A more accurate statement than "no two &mut at a time" is "no two &mut active at the same time". The compiler does indeed statically track these reborrows as part of borrow checking, so there's a tree of access leading back to the borrowed object.

3. Borrowing through a &mut is a form of reborrowing

So let child_mut = &mut parent_mut.child is in a sense a reborrow. You can think of it as the field access performing the actual reborrow.

4. The compiler is smart enough to know fields can't overlap and allows implicit "borrow splitting"

So you can have a &mut parent_mut.child and a &mut parent_mut.value at the same time.


I think that's what we need to understand the code.

    let parent_mut = &mut parent;
    parent_mut.value = 1;
    // get a reference, but not to parent.child, but to parent_mut.child. the former would give compile error
    // about having two mutable borrows
    let child_mut = &mut parent_mut.child; 
    child_mut.value = 2 + parent_mut.value;

    // up to now it seems that mutable references to parent and child can co-exist
    // but somehow this is not an example of multiple mutable references?

Here, child_mut is a reborrow of parent_mut, and the use of parent_mut.value is okay -- the field access is also a reborrow and, as it's to a different field, it doesn't overlap. You've basically split or reborrowed parent_mut into two non-overlapping &muts -- child_mut and a temporary that accesses the value field.

However, you still can't use the entirety of parent_mut and also keep the child_mut reborrow alive, because &mut parent covers (can be used to read) all of the fields, including the one child_mut refers to.

So even this won't work:

    parent_mut;
    child_mut;

As soon as you use the entirety of parent_mut, all of it's reborrows can no longer be alive, so you can't use child_mut any more.

And once you use parent, all of it's borrows -- transitively! -- can no longer be alive, so you can't create a new &mut parent and then use parent_mut or child_mut. That's also why you can't create child_mut from &mut parent.child and then use parent_mut again.

You need a tree of &mut from your owned object, and using any &mut in the tree, or the owned object at the root, "kills" all reborrows below that in the tree.


So, that's how you can understand why the body compiled up to the point of your playground, but you can't make any of the calls.

Indeed, it's impossible to call child_set_value with a &mut to a child and its parent, because the function is an API boundary -- the inside of the function gets to assume that there is no aliasing of the &mut Parent or the &mut Child. Using unsafe to create such aliasing is instant UB.

Therefore your attempts to make such a call can never succeed in safe code, because you need unsafe [1] to create UB in Rust.


  1. or a compiler bug ↩︎

9 Likes

What's going on in this case is that the single mutable reference parent_mut has been partially reborrowed to produce the mutable reference child_mut. At that point,

  • You cannot do anything with parent_mut as a whole until child_mut is no longer in use
  • But you can manipulate other fields of *parent_mut (value, in this case); only parent_mut.child is uniquely borrowed and thus unavailable for use except via child_mut.

The compiler tracks the borrow status of each field of a struct (or tuple, or enum variant, or even an array) separately so that you can do this. If you couldn't, it'd be pretty hard to write a useful fn do_something(&mut self) method!

The problem is not that new borrows are being created, but that parent_mut is (partially) borrowed and therefore you don't have it available to hand over to child_set_value. There's no way to write a function signature for child_set_value which expresses “mutable reference to everything but the child field of the Parent” — &mut Parent always means the whole thing, and if you don't have the whole thing you can't pass it.

Thank you for the reply! I will remember the term "partially reborrowed". I figured there was a name for this case

I confirmed this is the case in the Rust playground:

    let child_mut = &mut parent_mut.child; 
    parent_mut.child.value = 4; // error! cannot assign because it is borrowed already
    child_mut.value = 2 + parent_mut.value;

I will proceed to research RefCell and other workarounds, thanks again!

Thank you for your explanation, the example is clearer to me now. I will use the term "reborrow" in my google searches and that should help me find more discussion as well.

After looking at RefCell, it appears it cannot solve the issue either, since it just defers checking for duplicate mutable borrows at runtime, and the situation will remain at runtime: both child and parent will have active mutable references. Since child is contained in the parent struct, the child reference is a partial borrow of parent, and a panic will ensue if a mutable borrow is obtained from a RefCell to parent:

pub struct Child {
    value: i32,
    pub parent: Weak<RefCell<Parent>>,
}

impl Child {
    pub fn set_value(&mut self, value: i32) {
        self.value = value;

        // Child now decides it needs to alter a field on Parent...
        let parent_refcell = self.parent.upgrade().unwrap();
        // panic here
        let mut parent_mut = parent_refcell.borrow_mut(); 
        (*parent_mut).set_value(value + 10);
    }
}

The only way to invoke set_value is from a mutable reference to a child, which is presumably a partial borrow from a mutable parent.

My code is here:
Rust Playground

Well, what is the issue, exactly? If it's literally "have a &mut Parent and &mut Child of the parent's field usable at the same time," again, there is no solution -- if you accomplish it, you will have undefined behavior. That's a (severe) bug, not a solution.

If not, this may be an XY question.

If that's a logical presumption that is fair to make in your program or library, then Child::set_value should take a &mut Parent instead. Or you could take a pre-split borrow if that makes more sense for some reason.

That's an interesting idea, I didn't consider that was possible. Another idea I had was to put a closure as a parameter to child.set_value, and the closure implementation would have captured the parent as a mutable reference so it would be able to make the change to the parent value. But your idea is much more concise.

Sorry to sound like I'm making a frivolous XY question. The more pertinent issue is how to do OOP-style composition of classes in Rust. With composition, a parent class can delegate data and responsibility to its component classes. If a component class needs to make a change to its parent, composing class, how is that accomplished?

In a garbage collected OOP style language, it's trivial to accomplish, however it might not always be ideal as it creates a strong dependency/coupling between the parent/child classes. So if I were working in such a language, and I wanted to remove that dependency I would solve it by giving the child an indirect interface (or trait) instead of a direct instance to the parent. Or I would do an event dispatch or method callback.

Thank you for discussing with me as it's giving me some options to choose from!

Ultimately if the component class has many occasions where it needs to make changes to the parent composing class, then the best solution may be to move methods and responsibilities from the component class to the composition class.

The answer to that one is not easy, but trivial: don't.

If you consider the fact that Rust's design is built around explicit refusal to create such “dependency loops” then the answer is trivial, again: you don't do that.

Yup. And in Rust it's just simply not supported without complicated dance with Rc/Arc/Weak.

Not necessarily. But because Rust doesn't do OOP (at least it doesn't do classic “spaghetti of pointers” OOP) then it's usually good idea to start description with layman-level task (the one which you may discuss with someone who's not a software engineer) and then show us what design choices caused you to arrive at the point where you have these circular dependencies issue.

Don't worry, that happens insanely often with Rust newbies. In fact I was reluctant to learn Rust for years because I was sure that strict, rigid, ownership-and-borrow system would cause it not to be able to solve real-world tasks.

When I started using it I have found out that it works surprisingly well… but you can not just design something in the “spaghetti of pointers” OOP fashion and then just apply that to Rust.

You have to build that tree-of-trust (if you want to avoid using Rc/Arc) or DAG-of-trust (if Rc/Arc are acceptable) instead of just designing your objects and then designing the high-level which ties them together.

I hope I didn't strike a nerve mentioning OOP here. It's my position that OOP is a style of programming not necessarily dictated by the language one uses. For instance, you can write OOP in C. My takeaway so far is that it's possible to create a composition class, but that circular mutable references
require workarounds or design changes. Regardless, I still feel like I'd be writing OOP code.

I hope you mean don't do composition among classes that reference each other, and not "don't do composition at all".

It's one of the things I enjoy about Rust is that it encourages more thought around memory use.

You kinda did. It's extremely common desire of newbies and, well… it doesn't mix well with Rust's approach to data structures management. It's even mentioned as one specific mistake in the well-known article.

It's not that OOP is categorically impossible to do it Rust, but that you have to fight tooth and nail against Rust desire to resist “spaghetti of pointers” designs.

Oh, sure! C allows one to even make “spaghetti of code” designs, why would it resist “spaghetti of pointers” designs?

That's fine. You have been warned.

Well… usually OOP is characterised as trinity: Inheritance, Encapsulation and Polymorphism. But that's a lie. You can not have all three simultaneously!

But any two at the same time? Easy.

If you accept that and would decide which of these three you want to give in which situation — you would be able to design Rust programs, no problem.

Encapsulation+Polymorphism is done with traits. But there are no Inheritance (as you saw).
Inheritance+Polymorphism is done with traits with default methods. There are no Encapsulation.
Inheritance+Encapsulation is done with structs. But there are no Polymorphism.

Whether that may still be called an OOP is an open question.

1 Like

Thank you for the article, it's very good!

Your investigation rightfully tells you this should work, but it is prohibited for good reason. The type system is much too weak to express (safely) all the facts that the borrow checker knows about. Some other commenters have alluded to 'borrow trees' (which is what enables you to reborrow from the parent twice, at two seperate places) and the overly technical reason is that borrow trees are not expressible in function signatures, so cannot carry across abstraction boundaries.

The more practical reasoning is that a function signature like

fn child_set_value(child: &mut Child, parent: &mut Parent)

is a contract: this function may do anything with the parent reference, including accessing any of its subparts, like the child field. In particular, the compiler may not "peek inside" the function definition to see what parts of the contract it really does use. It must treat your implementation as completely equivalent to this one:

fn child_set_value(child: &mut Child, parent: &mut Parent)
{
  std::mem::swap(child, &mut parent.child);
}

which almost certainly does bad things at runtime if the references in fact alias.

The simplest solution is to split your data into more nested struct types:

struct ParentHeader {
  value : i32
}
struct Parent {
  header : ParentHeader,
  child : Child
}

// note the contract of this function is now more restricted:
// the parent arguement certainly may not access its `child' field!
fn child_set_value(child: &mut Child, parent: &mut ParentHeader)
{
...
}

I'll restate that this is simply a current limitation of the rust type system. But rust can express what you want to do, just not safely! I've seen at least 1,2 attempts to solve this problem in the more general case, but I could not recommend these - they are syntactically noisy and probably don't make life easier.

This is very much a separate problem. The rust ecosystem is very much dogmatically anti-OOP so you simply will not find a good answer to your question (or really any other answer other than "just don't do it"). But, you will have the original issue (expressing split borrowing across abstraction boundaries) whether or not you try to write "OOP-style" code in Rust.

1 Like

Thank you for the detailed reply

This is what I've concluded is the best path forward. It takes me time to figure out what additional structs are needed, or what methods should be extracted out from one struct to another, but I think over time it will become second nature.

I'm confused by this, but maybe it's because I don't consider inheritance as a core requirement for OOP. Rust has structs with member functions, and visibility specifiers, so the ability to encapsulate data and methods is built-in to the language. This and polymorphism tend to be what I consider most to be OOP.

When you say the community is anti-OOP do you mean also that the community is anti GoF design patterns as well?

It's more about the language enforcing composition over inheritance, rather than some dogmatic anti-OOP stance per se. (OOP is a very broad subject, but we might say for simplicity that Rust gets rid of the bad parts and keeps the good parts.)

Quite the opposite, I would say. The book you imply is probably one of the earliest sources of the "composition over inheritance" principle. Or at least one of the most influential.

For instance, the Rust community adopted the Builder pattern wholeheartedly. The language itself improves upon the pattern with typestate thanks to its ownership model.

3 Likes

No, I would even say that many of those design patterns are relatively straightforward to accomplish and are considered idomatic in Rust.

I mean dogmatic in the sense that mentions of "OOP" specifically in the context of Rust tend to have negative reponses along the lines of 'just don't do it'. Even though OOP is quite nebulous and, as you've mentioned, under some definition you could consider Rust an OO language
Anyways its just my observation. I am in no way speaking for the Rust community at large.

1 Like

Rust doesn't do implementation inheritance, basically. It's one of the most powerful tools in OOP arsenal yet also the most dangerous things one may do and no one invented a way to do it safely. Not in Rust and not in any other languages.

And Rust is about safety which means without finding a way to do that safely (which, again, no one managed to do in half-century of OOP existence) it couldn't accept it. What Rust does is very far from what Simula67 is does.

The question is whether OOP without implementation inheritance is still OOP or not can not be answered (because of nebulous definition of OOP) but since that's the original thing this is what is usually people understand under “OOP” moniker.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.