How does type inference work?

Consider this code:

fn main()
{
       {
        let mut _a = 11;
        let mut _b = 33;
        let mut _c = 55;
        
        let mut mut_ref_a = &mut _a;
        let ref_ref = &mut mut_ref_a;
        
        //let x = *ref_ref;                 // NO
        let x: &mut i32 = *ref_ref;       // YES

        *x = 123;

       
        println!("x: {}", x);
        println!("_a = {}", _a);
    }
}

Why does the compiler complain if I don't explicitly specify the type of variable x? For completeness: if I don't specify the type, the IDE still indicates the correct type with x: &mut i32 = ...

Thank you

1 Like

This is not a problem of type inference, but rather of implicit reborrowing.

When you have a &mut &mut T, you can't just move the internal &mut T out - this will leave the outer reference dangling. So what you can do is reborrow - that is, if you replace the failing line with let x = &mut **ref_ref;, it compiles, since in this case the borrow is not moved, but only temporarily locked.

This reborrowing process can also happen automatically, and that's what you see in your original working case. Although, this implicit reborrowing happens before type inference (logically - I'm not sure how it's done in the compiler, but logically it's this way), that is, to perform a reborrow, the type must be set explicitly, not inferred. Therefore:

  • when x is explicitly typed, the reborrow happens implicitly, all's good;
  • when reborrow is inserted explicitly, all's good;
  • when x is not explicitly typed and there's no explicit reborrow, compiler can't insert it implicitly, so it have to make a move - and this move is illegal.
5 Likes

@Cerber-Ursi described the problem well. I would like to add that this is a rather artificial problem. It doesn’t have to be this way, and there isn’t really anything to learn from this[1] except “Rust’s compiler implementation currently requires somewhat explicit/known types to make implicit re-borrows work”. I personally wish (and I wouldn't know why anyone should be opposed to) this code of yours to become accepted by the compiler without the type annotation, too, at some point in the future.


  1. unlike many other errors in Rust that exist to improve your code quality or maybe even outright prevent your code from badly misbehaving ↩︎

2 Likes

@Cerber-Ursi aren't references by themselves Copy types ? Did not get why reference has to be moved out. Could you explain that please ?

No, only shared immutable references (&T) are Copy types themselves. Exclusive mutable references (&mut T) aren't, because they can never “alias” i.e. active “mutable references” in Rust may never coexist with other active references (of any kind) to the same target, at the same time. Since Copying a value duplicates it, copying a references creates two references to the same target, which cannot work soundly for &mut T references, only for &T references.

4 Likes

I'm still a little confused by why the implicit move out of *ref_ref can't work; in general a move will invalidate the target, so is it the extra indirection here, or that Rust doesn't know how to invalidate both ref_refand mut_ref_a? I guess it boils down to if *&mut mut_ref_a is legal, but I'm on a phone right now...

It's not a move, it's a reborrow.

let x: &mut i32 =       *ref_ref;
let x: &mut i32 = &mut **ref_ref;

If we say x has type &'a mut i32 and ref_ref has type &'b mut &'c mut i32, the lifetime relationships are

'c: 'b: 'a

Sure, so I guess my question was "why is that?" - are derefs always reborrows, for example?

Rust never lets you move out of a mutable reference (here ref_ref) regardless of what the target type is — there is no transitive invalidation as you propose in this sentence. It is always true that once a mutable borrow ends, the referent is valid and available for use (or was also invalidated by something else at the same time).

So, every *some_mut_ref which successfully compiles must end up being a copy or a reborrow of the referent (or do nothing, e.g. let _ = *some_mut_ref;).

Implicit reborrows happen so you don't have to write things like this:

fn example(v: &mut Vec<String>) {
    // Without a reborrow, `v` would move when calling `v.push`...
    (&mut *v).push(String::new());
    // ...and this would be an error
    println!("{v:?}");
}

I.e. they make &mut _ act closer to as if it were Copy, so you don't have to think about it as often.

They happen when bindings are annotated (as that's a coercion site) and upon overwriting assignments (as part of NLL) and probably a number of other places. The presence of an explicit deref isn't required and I don't think it ever makes a difference. E.g. there is no explicit deref in this version.

(Unless the explicit deref is part of an explicit reborrow, of course.)

1 Like

I expect implicit derefs for method targets, though annotations being a coercion site helps to understand why they make it work here, too.

I was surprised that Rust would try to reborrow for let foo = *bar; rather than trying to move out of the target of bar and invalidating bar if that would invalidate it's target (eg it's not Copy). Reborrowing if the target is also a ref does make a common use easier, though.

It's function arguments generally, not just method receivers. At least in my mental model, this all fits because arguments are annotated bindings. (But knowing Rust, it's probably not actually that simple, even if it works out the same in this case. :slightly_smiling_face:)

Oh I meant implicit derefs in general are expected for targets, not just reborrows. I think method/field targets are the only place Rust does arbitrary implicit derefs like that?

All arguments do autoderef to a point.

fn by_value(_: i32) {}
fn by_ref(_: &i32) {}

fn main() {
    // Fails
    // by_value(&0);
    
    // compiles
    by_ref(&&&&&&&&&&&&&&&&&&&0);
}    

Note how it's not the same as what happens during method resolution since it doesn't go all the way down to the owned value. I think[1] it's something like

  • &[mut] &[mut] T to &[mut] T is fine
  • &[mut] T to &[mut] <T as Deref>::Target is fine
  • &[mut] DoesNotImplDeref (or NotARef) is the end of the line

(Note how NotARef would have to autoref to call Deref::deref or DerefMut::deref_mut when NotARef: Deref.)


If you're familiar with the reference, there's a potential point of confusion here (in the past it confused me). It implies that fully-qualified syntax doesn't have autoderef, but that's not true; instead it just acts like a non-method function call. So the receiver type is unambiguous, won't autoref, and won't autoderef all the way down to an owned value, but it still autoderefs.

Playground examples.


  1. based on experience, not documentation ↩︎

Huh, so it's that member access auto-refs that's the difference? I guess I sort of understood that in a vague hand wavey sense...

After spending awhile trying to suss out something simpler, I guess I would take back my "all arguments do autoderef to a point" comment and instead draw this distinction:

  • auto-deref is going from T to *T
  • deref coercion is going from &mut T to &mut *T or &mut T to &*T or &T to &*T
    • note how we have to start with a &[mut] and also end up with a &[mut]

Then I could reply, "the difference is that field access and method receivers use auto-deref, whereas normal arguments use deref coercion".

Or more cases with more words:

  • Field access (foo.field): has (transitive) auto-deref
    • lhs, *lhs, **lhs, until it bottoms out
    • No auto-ref (references don't have fields anyway)
  • Method resolution (foo.method()): has auto-deref and auto-ref
    • Same as field access for "initial candidates": lhs, *lhs, **lhs...
    • Adds auto-ref to each initial candidate
      • [lhs, &lhs, &mut lhs], [*lhs, &*lhs, &mut *lhs], ...
    • Plus some special-cased slice-unsizing stuff, minus non-slice unsizing, probably more nuances
  • Normal shared-ref args (arg: &T): has (transitive) deref coercion
    • &*arg, &**arg, &***arg, ...
    • No auto-ref
  • Normal exclusive-ref args (arg: &mut T): like shared-ref but mut, mostly
    • &mut *arg, &mut **arg, &mut ***arg, ...
    • However, starts acting like &T instead when Deref is possible but DerefMut is not
    • No auto-ref
2 Likes

Ha! I apologize for the nerd snipe! But did clear up some remaining "oogie boogie" feelings this all raised, so thanks for the report :saluting_face:

Hmm, a bit academic, but I always considered fields to be essentially (&[mut] self) -> &[mut] T

1 Like

You can move out of fields if nothing like Drop prevents it.

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.