[Solved] Why do references need to be explicitly dereferenced?


#1

I’m curious why Rust references need to be explicitly dereferenced, unlike references in C++. In C++ working with references to, for example, ints allows one to avoid multiple * operations.

Compare the following two code snippets.

C++:

void swap(int &a, int &b) {
    int temp = a;
    a = b;
    b = temp;
    return;
}

Rust:

fn swap(x: &mut i32, y: &mut i32) {
    let temp = *x;
    *x = *y;
    *y = temp;
}

Is it because Rust references are simultaneously filling the role of pointer and reference parameters from C++? Or perhaps the explicitness is preferred?

I’ve learned that method calls using . automatically dereference, so I’m curious if there are other cases when automatic dereferencing happens and why it is used in certain situations and not others.

I’ve opened a related documentation on the GitHub tracker: https://github.com/rust-lang/rust/issues/37413


When should the Copy trait be used?
#2

i’d avoid using C++ references to think about Rust references, they are two different things. So I’d treat & types in Rust like you would * types in C/C++, while keeping in mind the following points.

  1. A Rust & type is stored as a pointer, sometimes with a length and other information (see below responses).
  2. When you call foo.bar(), or access foo.bar, rust will automatically dereference foo if it has a type of &Foo.
  3. There is a trait called Deref that some smart pointer types implement, to change how the * operator works.

More details can be found in the book.


#3

While the former has some merit, it’s the latter reason, I believe. When reasoning about ownership transfer as enforced by the compiler, it really helps to be able to look at the code and tell whether one is moving the value or borrowing from it. Explicitly referencing (e.g. foo(&a, b)) really makes it obvious which is borrowed and which is moved. As you’ve noted, for ergonomics, &self is the exception — i.e. self.foo() can either borrow or move self. So I have to look up the docs for foo() to see whether I’m moving or borrowing a method invocation receiver, which can get tiring to check for unfamiliar code.

AFAIU, that’s not quite the case. There are 3 types of references (say, Struct is a struct, while Trait is a trait):

  • &Struct is just a pointer.
  • &Trait is a trait object (a pair of pointers: (1) one to an instance object of the trait, and (2) another to the trait vtable for the instance type).
  • &[T] is a pointer and a length.

(There are minor variations also: e.g. &StructT<Trait> is a trait object as well.)


#4

I think that’s the reason, kind of. But actually the only similarity between Rust references and C++ references is that they are non-null and always point to a valid object. On all the other aspects, Rust references work as C++ pointers, while the C++ references are actually lvalues exposed in the type system (Rust, of course, also has lvalues, but you can’t really express them as temporaries). Eg. you can do this with Rust refs and not with C++ ones:

let mut r: &i32 = &x;
r = &y;

The only other place I recall that does automatic dereferencing are “deref-coercions”. If the expression you have to write looks like that: &*****foo, you can just write &foo, ie. compiler would insert appropriate number of * for you (eg. for going from &&Vec<T> to &[T]). NB the star works both for simple deref (going from &T to T) and for custom Deref.

And why autoderef only in these two places? Probably to get most common and obvious cases work smoothly, while leaving the rest explicit and keep the language simple.

There are also some places that look like they do autoderef, but they’re not, eg.:

let a = 2 + &5; // It works because `i32` implements `Add<&i32>`
println!("{}", &&42); // It works because `Display` implementation for `&T`
                      // just calls implementation for `T`.

#5

It’s mentioned below, but I’d like to repeat it, since @marcianx said they weren’t sure. This isn’t the case. &T is a pointer. However, unsized types + a reference get stored as wide pointers: &Trait is a (vtable ptr, data ptr), and &str/&[T] is a (pointer, length).