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
T
and tries again. This uses theDeref
trait - ifT: Deref<Target = U>
then it tries again with typeU
instead ofT
. If it can’t dereferenceT
, it can also try unsizingT
. This just means that ifT
has 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>
.