The Dot Operator
The dot operator will perform a lot of magic to convert types. It will perform auto-referencing, auto-dereferencing, and coercion until types match. The detailed mechanics of method lookup are defined here, but here is a brief overview that outlines the main steps.
Suppose we have a function foo that has a receiver (a self, &self or
&mut self parameter).
If we call value.foo(), the compiler needs to determine what type Self is before
it can call the correct implementation of the function.
For this example, we will say that value has type T.
We will use fully-qualified syntax to be more clear about exactly which type we are calling a function on.
- First, the compiler checks if it can call
T::foo(value)directly. This is called a "by value" method call. - If it can't call this function (for example, if the function has the wrong type
or a trait isn't implemented for
Self), then the compiler tries to add in an automatic reference. This means that the compiler tries<&T>::foo(value)and<&mut T>::foo(value). This is called an "autoref" method call. - If none of these candidates worked, it dereferences
Tand tries again. This uses theDereftrait - ifT: Deref<Target = U>then it tries again with typeUinstead ofT. If it can't dereferenceT, it can also try unsizingT. This just means that ifThas a size parameter known at compile time, it "forgets" it for the purpose of resolving methods. For instance, this unsizing step can convert[i32; 2]into[i32]by "forgetting" the size of the array.
Here is an example of the method lookup algorithm:
let array: Rc<Box<[T; 3]>> = ...;
let first_entry = array[0];
How does the compiler actually compute array[0] when the array is behind so
many indirections?
First, array[0] is really just syntax sugar for the Index trait -
the compiler will convert array[0] into array.index(0).
Now, the compiler checks to see if array implements Index, so that it can call
the function.
Then, the compiler checks if Rc<Box<[T; 3]>> implements Index, but it
does not, and neither do &Rc<Box<[T; 3]>> or &mut Rc<Box<[T; 3]>>.
Since none of these worked, the compiler dereferences the Rc<Box<[T; 3]>> into
Box<[T; 3]> and tries again.
Box<[T; 3]>, &Box<[T; 3]>, and &mut Box<[T; 3]> do not implement Index,
so it dereferences again.
[T; 3] and its autorefs also do not implement Index.
It can't dereference [T; 3], so the compiler unsizes it, giving [T].
Finally, [T] implements Index, so it can now call the actual index function.
Consider the following more complicated example of the dot operator at work:
#![allow(unused)] fn main() { fn do_stuff<T: Clone>(value: &T) { let cloned = value.clone(); } }
What type is cloned?
First, the compiler checks if it can call by value.
The type of value is &T, and so the clone function has signature
fn clone(&T) -> T.
It knows that T: Clone, so the compiler finds that cloned: T.
What would happen if the T: Clone restriction was removed? It would not be able
to call by value, since there is no implementation of Clone for T.
So the compiler tries to call by autoref.
In this case, the function has the signature fn clone(&&T) -> &T since
Self = &T.
The compiler sees that &T: Clone, and then deduces that cloned: &T.
Here is another example where the autoref behavior is used to create some subtle effects:
#![allow(unused)] fn main() { use std::sync::Arc; #[derive(Clone)] struct Container<T>(Arc<T>); fn clone_containers<T>(foo: &Container<i32>, bar: &Container<T>) { let foo_cloned = foo.clone(); let bar_cloned = bar.clone(); } }
What types are foo_cloned and bar_cloned?
We know that Container<i32>: Clone, so the compiler calls clone by value to give
foo_cloned: Container<i32>.
However, bar_cloned actually has type &Container<T>.
Surely this doesn't make sense - we added #[derive(Clone)] to Container, so it
must implement Clone!
Looking closer, the code generated by the derive macro is (roughly):
impl<T> Clone for Container<T> where T: Clone {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
The derived Clone implementation is only defined where T: Clone,
so there is no implementation for Container<T>: Clone for a generic T.
The compiler then looks to see if &Container<T> implements Clone, which it does.
So it deduces that clone is called by autoref, and so bar_cloned has type
&Container<T>.
We can fix this by implementing Clone manually without requiring T: Clone:
impl<T> Clone for Container<T> {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
Now, the type checker deduces that bar_cloned: Container<T>.