Changing values through `Option<&mut T>` in a struct

I have some troubles with changing a value through a reference in a struct B, e.g.

I want my instances of the special (non-owning) struct A and B not to be mutable, because I am still under the impression I do not need to change the references &mut to point to some other stuff.
When I make o mutable I can use Some(v) = &o.i[0].as_deref_mut() where I can then change v (that is i),

struct A<'a> {
    i: [&'a mut usize; 2]
}

struct B<'a> {
    i: [Option<&'a mut usize>; 2]
}


fn main() {

    {
        let mut i = 3;
        let mut j = 5;
        let n = A {i: [&mut i, &mut j]};
        
        *n.i[0] += 3; // `n` does not need to be mutable.
    }
    
     {
        let mut i = 3;
        let mut j = 5;
        let o = B{i: [Some(&mut i), Some(&mut j)] };
        
        if let Some(v) = &o.i[0] {
            *v += 3; /// <<<<< FAILS: How to get to a type `&mut usize` but without making `o` `mut`.
        }
    }
}

You might be confusing several different things here.

First, immutability is transitive. If you insert even a single immutable reference into a stack of mutable references, you won't be able to mutate the referent at the end. x: &mut T makes *x mutable. x: &mut &mut T makes *x and **x mutable. However, x: &&mut T makes *x immutable (through that reference).

Second, in your code, you are not trying to change what the pointer points to. You are trying to add 3 to a reference itself. That doesn't make sense, Rust pointer arithmetic doesn't work like this (it doesn't exist on references, only on raw pointers).

So all in all, one possibility to make this compile is:

        let mut o = B{i: [Some(&mut i), Some(&mut j)] };
        
        if let Some(v) = &mut o.i[0] {
            **v += 3;
        }

This still doesn't change what *v points to. For that, you'd need to have another value and a mutable reference to that value, and then assign that mutable reference to *v (and not **v).

Indeed, I come from a C++ background and wasn't sure if in Rust & &mut T is the same as (T const *)* (T*) * const.

But isn't this a bit unfortunate, to change o.i[0] I must make o mutable.
To change n.i[0] I dont need n to be mutable. Suddenly changing from &mut T to Option<&mut T> requires a sudden change in mutability everywhere. Hm.

That is not correct. This is not about Option-vs-non-option. It's about the extra layer of indirection you introduced in the second example. In the first snippet, there's no extra layer of reference, but in the second one, you explicitly requested it by taking the address.

No, it's not "unfortunate", it's the very point.

It’s still barely possible to avoid the mut marker on o.

    {
        let mut i = 3;
        let mut j = 5;
        let o = B {
            i: [Some(&mut i), Some(&mut j)],
        };

        if let Some(&mut ref mut v) = o.i[0] {
            *v += 3;
        }
    }

Yes… the &mut ref mut v pattern looks a bit ridiculous, but it works :sweat_smile:

The fact that this works also relies on the fact that array-indexing is compiler-implemented and doesn’t use the IndexMut trait, otherwise, the indexing operation would require mutable access to the array itself.

6 Likes

Note that the mutability of bindings:

let mut o = B { /* ... */ };

can be considered effectively a lint, as you can always just

let o = B { /* ... */ };
let mut o = o;

The type of o is the same here. The mutability of the binding doesn't change the type. You just can't take a &mut to the o without the binding being mut.

This is in contrast with &'a T and &'a mut T which are distinct and very different types. You can't go from & to &mut.

So it's unfortunate in that you have to get use to specifying a mut binding when you need one, but there's not a deep semantic difference here. You still aren't able to modify *n.i[0] through a &A, for instance.

The main difference between the examples is that you didn't have to jump through hoops to get a mutable place expression without the Option in the way.


TL;DR, what you can or can't do with an immutably bound yet owned let x = X is not an indication of what you can or can't do with a &X.

3 Likes

& can be shared and is trivially copyable, while &mut by design is a guarantee of exclusivity. If &&mut didn't downgrade the inner reference to be shared like &&, then you could break the aliasing rules.

It's better to think of &mut == "mutex" rather than "mutable".

1 Like

That was exactl what I was looking for. I searched for away of matching the Option s inner &mut but without making o mutable. At this point I am wondering what is &mut ref mut?
Is it &mut &mut but circumventing the following

let temp: &mut &mut usize = o.i[0].as_deref_mut()

which needs o to be mutable (hm...)

I think of & in Rust as refcounted pointers with lifetime, where the compiler assures the invariants:

  • Only one write to the same storage or
  • Multiple reads to the same storage.

Should'nt be too wrong off of what it is, right?

Update: I missed to say compile-time ref-counted.

With two important differences:

  • They are compile-time refcounted, at runtime they're just pointers (or pointer+length, or pointer+vtable);
  • They can't stop the value they point to from destruction and therefore must all expire by the time owner does out of scope.
3 Likes

It's a pattern that will re-borrow the reference instead of trying to move it. The &mut part dereferences and the ref mut part captures by mutable reference. It's less weird looking in more normal patterns such as match (… as &mut Option<T>) { &mut Some(ref mut x) => …… }. In fact, such patterns used to be so common that at one point a rule was introduced to the language that you simply write Some(x) instead of &mut Some(ref mut x) or &Some(ref x), the former would previously have complained about trying to match a reference against a pattern for Option (essentially a type error).

1 Like

For other users: This goes deeply into reborrowing (vs. moving) which
as a start one might look here:

I have a hard time actually getting a grasp on it.

Basically it seems that reborrowing is the "reference-inspect" operation the compiler can introduce at some points contrary to a move of a &mut which will pass ownership (there can only be one (active) &mut). Reborrowing does not move the reference to another scope, but it creates another &mut with another referencial scope by looking at the original one where it points at, and the original &mut is set "on hold" (you can not use it until the new reborrowed &mut goes out of scope and the original becomes active again.

To understand the &mut part of patterns, see this article. I believe that's enough to understand that in

if let Some(&mut v) = o.i[0] {

the v corresponds to the place of the i: i32. Then ref mut takes a &mut to that place instead.


Default binding modes (aka "match ergonomics") makes pattern behavior contextual based on the type in a "usually what you want" but more-complicated-to-reason-about fashion.[warn(clippy::pattern_type_mismatch)] can point out when those are kicking in, if you use clippy.

2 Likes

Nice articel! Should be directly added to the rust book.

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.