patternrustCritical
What are Rust's exact auto-dereferencing rules?
Viewed 0 times
autoarerulesexactdereferencingrustwhat
Problem
I'm learning/experimenting with Rust, and in all the elegance that I find in this language, there is one peculiarity that baffles me and seems totally out of place.
Rust automatically dereferences pointers when making method calls. I made some tests to determine the exact behaviour:
`struct X { val: i32 }
impl std::ops::Deref for X {
type Target = i32;
fn deref(&self) -> &i32 { &self.val }
}
trait M { fn m(self); }
impl M for i32 { fn m(self) { println!("i32::m()"); } }
impl M for X { fn m(self) { println!("X::m()"); } }
impl M for &X { fn m(self) { println!("&X::m()"); } }
impl M for &&X { fn m(self) { println!("&&X::m()"); } }
impl M for &&&X { fn m(self) { println!("&&&X::m()"); } }
trait RefM { fn refm(&self); }
impl RefM for i32 { fn refm(&self) { println!("i32::refm()"); } }
impl RefM for X { fn refm(&self) { println!("X::refm()"); } }
impl RefM for &X { fn refm(&self) { println!("&X::refm()"); } }
impl RefM for &&X { fn refm(&self) { println!("&&X::refm()"); } }
impl RefM for &&&X { fn refm(&self) { println!("&&&X::refm()"); } }
struct Y { val: i32 }
impl std::ops::Deref for Y {
type Target = i32;
fn deref(&self) -> &i32 { &self.val }
}
struct Z { val: Y }
impl std::ops::Deref for Z {
type Target = Y;
fn deref(&self) -> &Y { &self.val }
}
#[derive(Clone, Copy)]
struct A;
impl M for A { fn m(self) { println!("A::m()"); } }
impl M for &&&A { fn m(self) { println!("&&&A::m()"); } }
impl RefM for A { fn refm(&self) { println!("A::refm()"); } }
impl RefM for &&&A { fn refm(&self) { println!("&&&A::refm()"); } }
fn main() {
// I'll use @ to denote left side of the dot operator
(*X{val:42}).m(); // i32::m() , Self == @
X{val:42}.m(); // X::m() , Self == @
(&X{val:42}).m(); // &X::m() , Self == @
(&&X{val:42}).m(); // &&X::m() , Self == @
(&&&X{val:42}).m(); // &&&X:m() , Self == @
(&&&&X{val:42}).m(); //
Rust automatically dereferences pointers when making method calls. I made some tests to determine the exact behaviour:
`struct X { val: i32 }
impl std::ops::Deref for X {
type Target = i32;
fn deref(&self) -> &i32 { &self.val }
}
trait M { fn m(self); }
impl M for i32 { fn m(self) { println!("i32::m()"); } }
impl M for X { fn m(self) { println!("X::m()"); } }
impl M for &X { fn m(self) { println!("&X::m()"); } }
impl M for &&X { fn m(self) { println!("&&X::m()"); } }
impl M for &&&X { fn m(self) { println!("&&&X::m()"); } }
trait RefM { fn refm(&self); }
impl RefM for i32 { fn refm(&self) { println!("i32::refm()"); } }
impl RefM for X { fn refm(&self) { println!("X::refm()"); } }
impl RefM for &X { fn refm(&self) { println!("&X::refm()"); } }
impl RefM for &&X { fn refm(&self) { println!("&&X::refm()"); } }
impl RefM for &&&X { fn refm(&self) { println!("&&&X::refm()"); } }
struct Y { val: i32 }
impl std::ops::Deref for Y {
type Target = i32;
fn deref(&self) -> &i32 { &self.val }
}
struct Z { val: Y }
impl std::ops::Deref for Z {
type Target = Y;
fn deref(&self) -> &Y { &self.val }
}
#[derive(Clone, Copy)]
struct A;
impl M for A { fn m(self) { println!("A::m()"); } }
impl M for &&&A { fn m(self) { println!("&&&A::m()"); } }
impl RefM for A { fn refm(&self) { println!("A::refm()"); } }
impl RefM for &&&A { fn refm(&self) { println!("&&&A::refm()"); } }
fn main() {
// I'll use @ to denote left side of the dot operator
(*X{val:42}).m(); // i32::m() , Self == @
X{val:42}.m(); // X::m() , Self == @
(&X{val:42}).m(); // &X::m() , Self == @
(&&X{val:42}).m(); // &&X::m() , Self == @
(&&&X{val:42}).m(); // &&&X:m() , Self == @
(&&&&X{val:42}).m(); //
Solution
Your pseudo-code is pretty much correct. For this example, suppose we had a method call
The core of the algorithm is:
Notably, everything considers the "receiver type" of the method, not the
It is an error if there's ever multiple trait methods valid in the inner steps (that is, there can be only be zero or one trait methods valid in each of 1. or 2., but there can be one valid for each: the one from 1 will be taken first), and inherent methods take precedence over trait ones. It's also an error if we get to the end of the loop without finding anything that matches. It is also an error to have recursive
These rules seem to do-what-I-mean in most circumstances, although having the ability to write the unambiguous FQS form is very useful in some edge cases, and for sensible error messages for macro-generated code.
Only one auto-reference is added because
Examples
Suppose we have a call
Suppose we have
(This answer is based on the code, and is reasonably close to the (slightly outdated) README. Niko Matsakis, the main author of this part of the compiler/language, also glanced over this answer.)
foo.bar() where foo: T. I'm going to use the fully qualified syntax (FQS) to be unambiguous about what type the method is being called with, e.g. A::bar(foo) or A::bar(&***foo). I'm just going to write a pile of random capital letters, each one is just some arbitrary type/trait, except T is always the type of the original variable foo that the method is called on.The core of the algorithm is:
- For each "dereference step"
U(that is, setU = Tand thenU = *T, ...)
- if there's a method
barwhere the receiver type (the type ofselfin the method) matchesUexactly , use it (a "by value method")
- otherwise, add one auto-ref (take
&or&mutof the receiver), and, if some method's receiver matches&U, use it (an "autorefd method")
Notably, everything considers the "receiver type" of the method, not the
Self type of the trait, i.e. impl ... for Foo { fn method(&self) {} } thinks about &Foo when matching the method, and fn method2(&mut self) would think about &mut Foo when matching.It is an error if there's ever multiple trait methods valid in the inner steps (that is, there can be only be zero or one trait methods valid in each of 1. or 2., but there can be one valid for each: the one from 1 will be taken first), and inherent methods take precedence over trait ones. It's also an error if we get to the end of the loop without finding anything that matches. It is also an error to have recursive
Deref implementations, which make the loop infinite (they'll hit the "recursion limit").These rules seem to do-what-I-mean in most circumstances, although having the ability to write the unambiguous FQS form is very useful in some edge cases, and for sensible error messages for macro-generated code.
Only one auto-reference is added because
- if there was no bound, things get bad/slow, since every type can have an arbitrary number of references taken
- taking one reference
&fooretains a strong connection tofoo(it is the address offooitself), but taking more starts to lose it:&&foois the address of some temporary variable on the stack that stores&foo.
Examples
Suppose we have a call
foo.refm(), if foo has type:X, then we start withU = X,refmhas receiver type&..., so step 1 doesn't match, taking an auto-ref gives us&X, and this does match (withSelf = X), so the call isRefM::refm(&foo)
&X, starts withU = &X, which matches&selfin the first step (withSelf = X), and so the call isRefM::refm(foo)
&&&&&X, this doesn't match either step (the trait isn't implemented for&&&&Xor&&&&&X), so we dereference once to getU = &&&&X, which matches 1 (withSelf = &&&X) and the call isRefM::refm(*foo)
Z, doesn't match either step so it is dereferenced once, to getY, which also doesn't match, so it's dereferenced again, to getX, which doesn't match 1, but does match after autorefing, so the call isRefM::refm(&**foo).
&&A, the 1. doesn't match and neither does 2. since the trait is not implemented for&A(for 1) or&&A(for 2), so it is dereferenced to&A, which matches 1., withSelf = A
Suppose we have
foo.m(), and that A isn't Copy, if foo has type:A, thenU = Amatchesselfdirectly so the call isM::m(foo)withSelf = A
&A, then 1. doesn't match, and neither does 2. (neither&Anor&&Aimplement the trait), so it is dereferenced toA, which does match, butM::m(*foo)requires takingAby value and hence moving out offoo, hence the error.
&&A, 1. doesn't match, but autorefing gives&&&A, which does match, so the call isM::m(&foo)withSelf = &&&A.
(This answer is based on the code, and is reasonably close to the (slightly outdated) README. Niko Matsakis, the main author of this part of the compiler/language, also glanced over this answer.)
Context
Stack Overflow Q#28519997, score: 246
Revisions (0)
No revisions yet.