Some unexpected borrow checker behavior that lead to a hard to find bug

Consider the following three functions, which differ only in the second line:

fn foo() {
    let mut a = 1;
    let ref b = if true { a } else { 0 };
    let c = &mut a;
    *c += 1;
    dbg!(b);
}

fn bar() {
    let mut a = 1;
    let b = if true { &a } else { &0 };
    let c = &mut a;
    *c += 1;
    dbg!(b);
}

fn baz() {
    let mut a = 1;
    let ref b = a;
    let c = &mut a;
    *c += 1;
    dbg!(b);
}

These all appear to do the same thing: create a reference, create and use a mutable reference to the same data, then use the original reference, which of course should be a compile time error. However, bar() and baz() give a compiler error as expected, but foo() instead makes b point to an implicit copy of a, so the program runs and prints b = 1. Seeing how foo() is halfway between bar() and baz(), this behavior took me by surprise and lead to a difficult to find bug.

I can kind of understand why this is happening: in foo(), b is a reference to the result of the if expression, and the if expression is taking the arguments a and 0 by copy. So I guess the behavior is logical. My initial reaction was that this was a mistake in Rust that should be fixed. It would have been nice if there was an easier way to find this, but I guess I understand it now. What are your thoughts?

It's a pattern behavior, really. It triggers this clippy lint.

1 Like

I have never tried to write a let statement like that and frankly I'm surprised it is allowed. I don't see what value it has over this:

let b = &a;

The thing between let and = is a pattern and this is just a normal part of pattern syntax.

Usually you'll see it in code which takes part of an object by reference (e.g. a String field) to avoid moving, while others can safely be bound by value because they are Copy.

let Person { ref name, age } = ...;

// or

let result = expensive_operation();
if let Ok(Person { ref name, age }) = result {
  println!("{} is {} years old", name, age);
}
return result;
2 Likes

Cool. I forget sometimes that you can pattern match like that.

While I'm not surprised by it, I do wish we more aggressively discouraged its use in that form. There are a ton of beginner questions about it, and it's an irrelevant distraction.

(Personally I try to just never use ref, but I know that's a controversial opinion. But AFAIK everyone agrees that let x = &a; should be used over let ref x = a; for those specific non-nested forms.)

I wouldn't go as far as actively telling people to avoid it, but I'd definitely nudge people towards more common forms like let x = &y, throwing around words like "more readable" and "idiomatic".

With match ergonomics you don't really need ref apart from a handful of niche cases.

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.