Ref keyword versus &

One thing I'm not clear on is why Rust has both a "ref" keyword and an & operator? For example, in match arms I seem to need "ref" instead of & but I'm not sure what the difference is supposed to be.

7 Likes

In patterns, & destructures a borrow, ref binds to a location by-reference rather than by-value. In other words, & lets you reach through a borrow, and ref says "take a borrow to this place within the thing I'm matching".

6 Likes

& and ref are opposites.


#![feature(core_intrinsics)]

fn main() {
    let x = &false;
    print_type_name_of(x);

    let &x = &false;
    print_type_name_of(x);

    let ref x = &false;
    print_type_name_of(x);
}

fn print_type_name_of<T>(_: T) {
    println!("{}", unsafe { std::intrinsics::type_name::<T>() })
}
&bool
bool
&&bool
32 Likes

Now that match ergonomics is stable, you can just pretend that ref doesn't exist, and your life will be better :slightly_smiling_face:

(And I'm filing issues to have compiler suggestions also pretend that it doesn't exist.)

7 Likes

ref on the left side of = is the same as & on the right.

let ref x = 1;
let x = &1;

& on the left side of = is the same as * on the right.

let &y = x;
let y = *x;
61 Likes

Thanks re: the match ergonomics and the clarification of ref and & on left vs right.

Also, I'm surprised that let x = &false; or similarly let x = &1; above compiles because I would have thought that false is a compile-time literal, rather than a variable in memory to which you can create a reference. In these cases, is the compiler actually storing false/1 in a hidden data segment memory location, so the program can create a reference to them?

My C intuition is getting in my way here because I see the following also work, the type of x is always &&bool, and if I println! the value of x at each line, it happily displays false.

    let ref x = &false;
    let ref x : &bool = &false;
    let x : &&bool = &&false;

Doesn't a variable of type &&bool have to be dereferenced twice to get to the actual bool value (through 2 levels of pointer indirection)?

3 Likes

println!() macro does magical auto-dereferencing. Similarly (&&&&&&&&&&&&&&&1).to_string(); keeps dereferencing until it finds that &i32 has to_string().

1 Like

Don't C and C++ do this exact trick with string literals? When I noticed Rust could do this, my first impression was that Rust was simply being a little more consistent, making string literals a little less magical.

1 Like

This is https://github.com/rust-lang/rfcs/blob/master/text/1414-rvalue_static_promotion.md

2 Likes

Not very helpful in understanding concept.
T, &T, &mut T are data types. On right in expression they are constructors. On left they pattern match the data. The default is to move the variable on the right, i.e. if it is not a Copy type and declared with let in previous lines you no longer can access it afterwards. ref/ref mut borrows from that variable instead. Especially with Copy type a ref mut allows the original fields to be changed. A basic structure you can just use structure.field = but enums require this added syntax.

let mut opt = Some(1);
// if let Some(ref mut val) = opt { //old style
// as @scottmcm mentions Now that match ergonomics is stable
if let /* &mut */ Some(val) = &mut opt { // ref mut is now implied, but can add if want
    *val = 2;
}
println!("{:?}", opt);

Yes, it's a feature, either uses on the stack just above in a hidden variable (e.g. let x = &-1i32.abs()) or in those literal cases static memory.

3 Likes

Just to add/clarify, it’s not just literals. You can, eg, apply this to a const-fn return value or more generally, anything that can be const (modulo the unsafe cell restrictions).

1 Like

No, in C++ you can't create a reference or pointer to a literal.

Thanks everyone for these answers. Just one more thing though, am I correct that the following line results in not one but 2 hidden variables being created in order to support the required levels of dereferencing? (the first to store false and the second to store a reference to the first)

let ref x = &false; // type of x is &&bool

In theory, perhaps. But do remember that the compiler is smart enough to determine your actual requirements and eliminate any unnecessary allocations or stores/loads. Lifetime analysis plays a big part in such elimination.

Edit: Another way to view this is that *, &, ref, etc. convey semantic meaning to the compiler. Unlike c, c++, etc, they do not directly instruct the compiler to generate or dereference pointers. The fact that the Add trait's impl specifies an & input parameter does not inherently cause the compiler to generate a value in a memory location and then compile an add-from-memory instruction.

2 Likes

To add to @TomP "compiler is smart enough to determine your actual requirements";
clippy is a lint tool that can warn about unneeded & and many other non-idiomatic code use. Currently needs nightly. Available on play too.

rustup update nightly
rustup component add --toolchain nightly clippy-preview
cargo +nightly clippy
1 Like

Why unlike? C or C++ compilers can do the same optimizations in this regard.

Of course they can. But stating that fact would have required me to write a much longer answer. The reality is that all programming languages express intent to their compilation tool, which is free to optimize within usually-knowable constraints. That's even true for assemblers, which often choose an appropriate address-syllable form based on other aspects of the code (e.g., branch distance or data-item index and item size).

Right but I wasn’t sure why you chose to say “unlike”, which makes it sound like this optimization is unique to Rust with respect to those languages. But I think we’re good now :slight_smile:

I used "unlike" because I feel that in c and similar languages the addressing-related modifiers express intent with respect to the implementing code much more than they do in Rust. Rust uses & to indicate that ownership is not being transferred; it does not use & to instruct the compiler to pass an address. In fact in the case of & (but not &mut) a common compiler behavior for simple items and expressions of small storage size is to pass them by value.

2 Likes

I think I agree with you for C, but I think the distinction is blurrier for something like C++.

Yeah, that's one way to look at it. Another is that the data behind the pointer is aliased, and this is an explicit way to inform the compiler about it.

In the presence of an optimizer, this is somewhat moot. In Rust, mechanically, it's passing a pointer - a debug build will pass a pointer, even if a release/optimized build wouldn't. In a release build, if the callee smuggles this reference somewhere that isn't inlined or forms a raw ptr off it, then the indirection has to stick around. No different than C++, really.

Anyway, I think we're splitting hairs and likely distracting this thread, and I think I understand your rationale now.

1 Like