Why does this compile? Automatic dereferencing?

I'm surprized to see that

fn main() {
    let i = 23;
    let x: &i32 = &i;
    let y: &i32 = &x;  // <-- what kind of magic is happening here?!
    println!("Hello, world! {} {}", x, y);
}

actually compiles. I would have expected the compiler to complain about initializing a &i32 with a &&i32. Is this automatic dereferencing in action? Since when is it applied in such situations? It seems new to me.

3 Likes

Might have to do with coercions.

2 Likes

Thanks for the hint. Yeah, this is definitely a context in which coercion is allowed. But this RFC does not include a &&T to &T coercion rule. By transitivity, &&&T to &T would also work (and it does, I just checked).

1 Like

Definitely has to do with coercions / dereferencing.

Deref coercions is the current documentation I believe, though the rfc linked by @sinistersnare has a more technical view.

In short, since rust knows that you want an &i32 exactly, it will apply the * operator any amount of times when you use &. In your example, let y: &i32 = &x is equivalent to let y = &*x. This is mainly useful for dereferencing things which implement the Deref trait, but it will also "dereference" any number of references as long as you (or a method deceleration) specifies the exact definition, and you use & (or &mut) to create the reference.

1 Like

That was the first thing I checked ... whether there is some kind of Deref impl for &'a &'b T to &'a T. But there is not and that type of coercion would actually be inferior (we get &'a T instead of &'b T with 'a being possibly shorter than 'b).

I guess this &&T to &T coercion is built in to the compiler and not yet documented.

Yeah, that is built into the compiler. I'm not sure where it's documented, but it kind of does make sense just because of the name. The dereference operator, in essence, removes a reference (de-references). It's just overloaded to use the Deref trait as well.

That documentation does mention this behavior a little bit in the end, with:

It never mentions this behavior in other places though, like when using the & operator with a known type of variable.

So far, this…

has only been documented in combination for method-call contexts (called "receiver coercion").

This here

let _: &i32 = &&24;

is no such context. But it's still a context in which other coercions are allowed according to RFC0401. As for "the compiler will insert as many * as necessary" this is not exactly true because this here

let _: i32 = &24;

won't work anymore. The compiler is apparently only happy about peeling off top-level references as long as the result is still a reference.

So we have the impl:

impl<'a, T> Deref for &'a T {
    target = T;
    ...
}

So if our target is &i32, and we give it a &(&i32), it will coerce the &T (&&i32) -> T (&i32)

Im probably wrong. @nrc does not seem to be on users discourse, but you may be able to ping him on IRC?

The thing is the compiler will insert "as many * as necessary" before the &. Inserting more *s still wouldn't work in your example, as the following code doesn't compile:

let _: i32 = &*24;

It does insert as many *s as needed - it just only inserts them logically before the & operator you are using, not after.

1 Like

If this was done with a non-Copy type, it would be moving out of a non-exclusive reference.

struct Foo;
fn main() {
    let i = Foo;
    let j: Foo = *&i;
}

That doesn't compile.

struct Foo;
fn main() {
    let i = Foo;
    let j: &Foo = *&&i;
}

That does.

I know that it can't work for non-Copy types. I guess what you are saying is that if it can't work for non-Copy types, it shouldn't work for Copy types either? Seems like a weird argument.

Anyhow, all I was looking for is an explanation of why something like this

let s: &'static str = "hello";
let p = &p;
let t: &'static str = p; // MAGIC

compiles, specifically the last line. With the help of you guys I think I finally came to the right conclusion: It is a kind of "deref coercion" that -- in this case -- does not involve any Deref trait implementation, in particular, it does not involve this one...

impl<'a, T> Deref for &'a T {
    type Target = T;
    fn deref(&self) -> &Target;
}

If the x in the expression *x is a reference, it is simply dereferenced without any involvement from any Deref trait. The compiler already knows how to do that. The Deref trait implementations are for those kinds of receivers that are not references. The above implementation is probably just there for uniformity so you can still invoke the deref method manually if you want to. But for references x this method is not equivalent to &*x. There is a difference:

use std::ops::Deref;
fn main() {
    let s: &'static str = "hello";
    let x: &'static str = &*s;
    let y: &'static str = s.deref();  // <-- lifetime error
}

If you dereference a shared reference, you're not borrowing it. But if you call deref() on it, you are borrowing the reference itself which gives is this lifetime error.

The code

let s: &'static str = "hello";
let p = &p;
let t: &'static str = p; // MAGIC

works because the last line is mapped to

let t: &'static str = &**p;

as per "deref coercion rules" which doesn't involve any Deref trait implementations in this case because we would get a lifetime error otherwise.

This is because of Deref Conversions (RFC #241), which are not the same as Deref Coercions (RFC #401), even though they rhyme. (Rust poets and rappers, take note.)

Deref Conversions allow converting from &T to &U when T: Deref<U>. Unlike deref coersions, deref conversions are not limited to method calls and field access.

In this case, T is &i32 and U is i32. The conversion is from &T (&&i32) to &U (&i32) is allowed because T implements Deref<U> (&i32 implements Deref<i32>).

This is the same feature that allows conversions from &String to &str, or &Vec<u8> to &[u8].

5 Likes

This was my thinking, except that the example involving 'static rules it out. <&T>::deref has the signature (&'a &'b T) -> &'a T, but the deref coercion (note that it's referred to as a coercion in RFC #241 despite the title) here seems to be doing (&'a &'b T) -> &'b T, which doesn't make sense if the deref coercion is implemented purely in terms of Deref::deref.

For clarity, I'll introduce the operator @: &T -> T to represent the primitive dereferencing of & types. * is an overloaded operator. The first version has the signature *: T -> U, where T: Deref<Target=U>, and is implemented by the substitution *expr => @Deref::deref(&expr). The second version is *: &T -> T, implemented by the substitution *expr => @expr. This is essentially equivalent, except that in the first version, &*expr has to have the same lifetime as &expr, whereas, in the second version, &*expr turns into &@expr and so it has the same lifetime as expr does.

With this in mind, the deref coercion seems to be implemented by the substitution expr => &*@expr. Where expr: &'a T and T: Deref<Target=U>, this expands to expr => &@Deref::deref(&@expr), or just expr => Deref::deref(expr), as expected, since & and @ are inverse operations. However, where expr: &'a &'b T, this expands to expr => &@@expr, or just expr => @expr. This latter substitution has the signature (&'a &'b T) -> &'b T, which explains why the 'static example works.

2 Likes