Search code examples
rusttuplesdestructuringmutable

Can I simplify tuple expression that uses a double reference?


I have an expression that creates variables that are double references, only to have to dereference the variable to use it in a call. I suspect there is a simpler syntax and way to call my function.

I have two types A and B, neither movable, but both cloneable. I am calling a function with a Vec of tuples of A and B and need to index the Vec to get out the tuple, destructure the values from the tuple into local variables, and use one value immutably but the other mutably in another function call.

  pub fn my_func(v: &mut Vec<(&A, &mut B)>, x: &mut C) {
    let i = 0_usize;        // Would be a loop index normally.
    let (a, b) = &mut v[i]; // This makes b a double reference.
    
    x.do_something(*b);     // This expects &mut B as parameter.
  }

How can I change the signature to my_func, the indexing of v and the destructuring into a and b in a consistent way to simplify things? Can I get away with fewer ampersands and muts and derefs? Note that I do not need a to be mutable, just b.

Ignoring &mut C since it is required, if you count one point for each ampersand &, one point for each mut, and one point for each deref *, then you get 8 points. A solution that scores fewer "points" is a winner.


Solution

  • I don't think there is any way to simplify the function's signature. You could replace Vec<T> with [T], provided you don't need to push or pop elements from the vector, but this change doesn't affect the "score" per your definition.

    Both a and b turn into double references because of match ergonomics.

    The pattern on the left side of the let doesn't have exactly the same "shape" as the expression on the right, so the compiler "fixes" it for you. The type of the expression is &mut (&A, &mut B). The pattern doesn't match the outer &mut, so the compiler pushes it inside the tuple, giving (&mut &A, &mut &mut B). Now the shapes match: the outer type is a 2-tuple, so a is &mut &A and b is &mut &mut B.

    I've come up with a few variations on your function:

    pub struct A;
    pub struct B;
    pub struct C;
    
    impl C {
        fn do_something(&mut self, b: &mut B) {}
    }
    
    pub fn my_func_v1(v: &mut Vec<(&A, &mut B)>, x: &mut C) {
        let i = 0_usize;
        let (a, b) = &mut v[i];
    
        x.do_something(b);
    }
    
    pub fn my_func_v2(v: &mut Vec<(&A, &mut B)>, x: &mut C) {
        let i = 0_usize;
        let &mut (a, &mut ref mut b) = &mut v[i];
    
        x.do_something(b);
    }
    
    pub fn my_func_v2a(v: &mut Vec<(&A, &mut B)>, x: &mut C) {
        let i = 0_usize;
        let (a, &mut ref mut b) = v[i];
    
        x.do_something(b);
    }
    
    pub fn my_func_v3(v: &mut Vec<(&A, &mut B)>, x: &mut C) {
        let i = 0_usize;
        let (a, b) = &mut v[i];
        let (a, b) = (*a, &mut **b);
        // Or:
        //let a = *a;
        //let b = &mut **b;
    
        x.do_something(b);
    }
    
    pub fn my_func_v4(v: &mut Vec<(&A, &mut B)>, x: &mut C) {
        let i = 0_usize;
        let e = &mut v[i];
        let (a, b) = (e.0, &mut *e.1);
        // Or:
        //let a = e.0;
        //let b = &mut *e.1;
    
        x.do_something(b);
    }
    

    v1 is identical, except I wrote b instead of *b. The compiler sees an expression of type &mut &mut B and a parameter of type &mut B, and will transparently dereference the outer reference. This might not work if the parameter type is generic (here, it's the nongeneric &mut B).

    v2 is the "direct" way to avoid making a and b double references. As you can see, it's not very pretty. First, I added &mut in front of the tuple, in order to match the type of the expression on the right, to prevent match ergonomics from kicking in. Next, we can't just write b because the compiler interprets this pattern as a move/copy, but we can't move out of a mutable reference and &mut T is not Copy. &mut ref mut b is the pattern version of a reborrow.

    v2a is like v2, but with the &mut removed on both sides (thanks loganfsmyth for the reminder!). v[i] is of type (&A, &mut B), so it cannot be moved, but it's an lvalue, so if we re-reference parts of it that cannot be moved/copied, then the whole thing won't be moved at all.

    Remember that in patterns, &mut deconstructs a reference, while ref mut constructs a reference. Now, &mut ref mut might seem like a noop, but it's not. A double mutable reference &mut &mut T actually has two distinct lifetimes; let's name them 'a for the inner lifetime and 'b for the outer lifetime, giving &'b mut &'a mut T ('b is shorter than 'a). When you dereference such a value (either with the * operator or with a &mut pattern), the output is &'b mut T, not &'a mut T. If it were &'a mut T, then you could end up with more than one mutable reference to the same memory location. b yields a &'a mut T, while &mut ref mut b yields a &'b mut T.

    v3 uses the dereferencing operator instead of &mut patterns, which is hopefully easier to understand. We need to explicitly reborrow (&mut **b), unfortunately; *b is interpreted as a move out of a mutable reference, again.

    When b is &mut &mut B and we pass either b or *b to do_something, neither of these two are actually "correct". The correct expression is &mut **b. However, the compiler automatically references and dereferences arguments (including the receiver) in function/method calls, but not in other contexts (such as the initializer for a local variable).

    v4 saves a couple * by relying on the auto-deref from using the . operator.