Clarification of "Where's the `->` Operator" section of Chapter 5.3

I need some help understanding the "Where’s the -> Operator?" of Chapter 5.3 (emphasis added):

Where’s the -> Operator?

In C and C++, two different operators are used for calling methods: you use . if you’re calling a method on the object directly and -> if you’re calling the method on a pointer to the object and need to dereference the pointer first. In other words, if object is a pointer, object->something() is similar to (*object).something().

Rust doesn’t have an equivalent to the -> operator; instead, Rust has a feature called automatic referencing and dereferencing. Calling methods is one of the few places in Rust that has this behavior.

Here’s how it works: when you call a method with object.something(), Rust automatically adds in &, &mut, or * so object matches the signature of the method. In other words, the following are the same:

p1.distance(&p2);
(&p1).distance(&p2);

The first one looks much cleaner. This automatic referencing behavior works because methods have a clear receiver—the type of self. Given the receiver and name of a method, Rust can figure out definitively whether the method is reading (&self), mutating (&mut self), or consuming (self). The fact that Rust makes borrowing implicit for method receivers is a big part of making ownership ergonomic in practice.

I'm not sure I understand the example with p1.

  • Is it just showing that the semanitcs of . applied to an object are the same as those of . applied to a reference to that object? So, assuming distance() borrows self immutably and p1 is of type T, then is this example showing that p1.distance(&p2) is equivalent to T::distance(&p1, &p2) (where the p1 is first transformed to &p1 via automatic referencing)?

Also, regarding the last two lines of the quoted excerpt:

  • I don't understand the meaning of "clear receiver". Why isn't automatic referencing and dereferencing enabled for regular functions? In what way do they not have a clear receiver?
  • What does "making ownership ergonomic" mean?

yes, well, correct but incomplete. the "auto referencing and dereferencing" feature means, between the types of the argument and the parameter, as long as you can get one from the other though a chain of adding or removing levels of references, the compiler will do it automatically so you don't need to have the exactly type to call the method.

for example, if the receiver parameter type is &Self, the following is valid:

struct Object {}
impl Object {
    fn foo(self: &Self) {}
}
let x = Object {};
x.foo(); // x has type `Self`, need `&Self`, so compiler add a `&` automatically, i.e. auto referencing
(&x).foo(); // exactly match
(&&x).foo(); // (&&x) has type `&&Self`, need `&Self`, so compiler "remove" one level of `&`, i.e. auto dereferencing
(&&&&&&&&&x).foo(); // auto dereferencing many times

correct.

the term receiver in rust has a specific meaning in this context, it refers to the self keyword (as function parameter). it is not a general term in the sense of similar concept in other OOP languages. the receiver cannot have arbitrary types, and it has short hand syntax for most common use cases (e.g. &self is short for self: &Self), the full list of allowed types for a receiver is documented here

it just means the programmer doesn't need to remember the exact type of some expression (i.e. how many levels of references it contains) to be able to call a method, as opposed to, e.g. C++ where you need to remember how many indirection levels.

say in C++ if you have Object x; or Object &x, you call methods with the dot operator; if you have Object *x;, you call methods with the arrow operator (or dereference it first); if you have Object ***x, you MUST call it like (**x).foo() or (*x)->foo(). in rust, you just x.foo(), no matter how many levels of references the type of x has. that's what "ergonomic" means in this context.


a small side note, the auto dereferencing always applies, so you'll never get a type mismatch errors like expected Self, found &Self, but you can still get other errors if rules are violated, e.g. in the following snippets, you'll get a "can't move out of *x" error, and a can't borrow x as mutable error:

struct Object {}
impl Object {
    fn foo(self) {}
    fn bar(&mut self) {}
}
fn main() {
    let x = Object{};
    {
        let x = &x;
        x.foo(); // can't move out of *x
    }
    x.bar(); // can't borrow x as mutable
}
2 Likes

They are not always the same. Method lookup always tries an exact match first, so if two methods with the same name apply, and one has a receiver type of Foo and the other has a receiver type of &Foo, then the type can affect which one is called. (This can only happen when traits are involved.)

But in most code, you can ignore the difference, because method lookup will find the method you meant.

3 Likes

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.