Dereference confusion - pointer like or alias like

When I am learning about the rust references, I am confused with the syntax used for dealing with references. specifically when the intention is to de-reference them.

e.g. following code:

fn print_i32_ref(val: &i32) {
    println!("The value is {} and {}", val, *val);
}

fn inc_i32_ref(val: &mut i32) {
    *val += 1;
    println!("The value is {} and {}", val, *val);
}

fn main() {

    let mut x = 5;
    print_i32_ref(&x);
    inc_i32_ref(&mut x);
}

Here it is confusing to see that all the functions are de-referencing val but rust mandates use of * only for the mutable operations and its okay to skip the * when using in read-only manner

print_i32_ref uses them like C++ aliases (no need to use * operator)
inc_i32_ref uses them like C pointers (need to use * operator) and sometime like C++ aliases (no need to use * operator)

Is rust compiler magically adding the missing * operator?

The reason why you can omit * is that the println! macro works on references but also contains some magic to only take a reference, even if you omit the &.

The exclamation mark (!) serves as a hint that tricks like these are to be expected, as println! is not an ordinary function.

1 Like

Oh wow! that makes sense. The println! is doing the some magic that I will learn later

I tried to modify the function in an attempt to use them like C++ aliases like so

fn print_i32_ref(val: &i32) {
    let mut another = 7;
    another = val;
    println!("Another is {another}");
}

and this one gave compiler error. So it's always required to use * while dereferencing all types of references.

Thanks for the quick reply!

  • cheers

println!() isn't; it's just that there is a Display impl for &T when T: Display.

Auto-dereferencing happens in Rust in a number of cases, for example, field and method access. However, this is not affected by mutability. The rules are the same for mutable and immutable references.

In general, it's safest to assume that Rust references work like C's pointers. They bear basically no similarity to C++ "references". C++ references are not first-class types, whereas Rust references are real, regular types. Rust has no comparable concept of "aliases" (and the language and its users don't miss it).

As mentioned before, Rust has some syntactic sugar for avoiding explicit dereferencing in some, limited cases, and there are similar blanket impls for &T, &mut T, Box<T>, and other pointer-like types for most traits, if they make sense. But Rust references are pretty much different from the values they point to, and you should treat them accordingly. When in doubt, be explicit about referencing and de-referencing.

Note that this is not why you can omit dereferencing. This would be a reason for omitting explicit referencing. However, after the implicit additional layer of reference introduced by println!(), OP's code results in &&i32 being printed, which works specifically due to the aforementioned blanket impl.

5 Likes

Note that there are some more situations where (de)reference happens automatically.

For example, &&&&&&&&T gets automatically coerced to &T where necessary:

fn print_i32_ref(val: &i32) {
    println!("The value is {} and {} and {}", val, *val, &&&&val);
}

fn inc_i32_ref(val: &mut i32) {
    *val += 1;
    println!("The value is {} and {} and {}", val, *val, &&&&val);
}

fn main() {
    let mut x = 5;
    print_i32_ref(&x);
    print_i32_ref(&&&x); // this works
    //print_i32_ref(x); // but this doesn't
    inc_i32_ref(&mut x);
    inc_i32_ref(&mut &mut &mut x); // this works too
    //print_i32_ref(x); // but this doesn't
}

(Playground)

Note that you can also see in this example (if you comment-in the commented-out lines), that function arguments which take a reference require at least one & to turn the value into a reference.

Another situation where & and * can be ommitted is the receiver for method calls (i.e. the value that you write left of the "."):

fn main() {
    let v = vec![10, 20, 30];
    let l1 = (&v).len(); // we could write this
    let l2 = v.len(); // but we normally just write this, as the `&` is added automatically

    let r = &v;
    let l3 = (*r).len(); // we could write this (as the `&` is added automatically)
    let l4 = r.len(); // but we normally just write this
    let l5 = (&r).len(); // and we can also still write this

    let b = Box::new(r);
    let l6 = (**b).len(); // we could write this (as the `&` is added automatically)
    let l7 = (*b).len(); // we also could write this
    let l8 = b.len(); // but we even can write this, because the `*` to dereference the box is also added automatically
    let l9 = (&b).len(); // and, of course, we could also write this
    
    assert_eq!(l1, 3);
    assert_eq!(l2, 3);
    assert_eq!(l3, 3);
    assert_eq!(l4, 3);
    assert_eq!(l5, 3);
    assert_eq!(l6, 3);
    assert_eq!(l7, 3);
    assert_eq!(l8, 3);
    assert_eq!(l9, 3);
}

(Playground)

The exact rules are explained here:

Method-call expressions

[…]

When looking up a method call, the receiver may be automatically dereferenced or borrowed in order to call a method. This requires a more complex lookup process than for other functions, since there may be a number of possible methods to call. The following procedure is used:

The first step is to build a list of candidate receiver types. Obtain these by repeatedly dereferencing the receiver expression's type, adding each type encountered to the list, then finally attempting an unsized coercion at the end, and adding the result type if that is successful. Then, for each candidate T, add &T and &mut T to the list immediately after T.

This confused me when I was beginning to learn Rust, because it felt like I don't need & or *.

But this isn't generally true (in particular not for method/function arguments), as you can see in the first example that the following code would fail:

     print_i32_ref(&x);
     print_i32_ref(&&&x); // this works
-    //print_i32_ref(x); // but this doesn't
+    print_i32_ref(x); // but this doesn't

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types
  --> src/main.rs:14:19
   |
14 |     print_i32_ref(x); // but this doesn't
   |     ------------- ^
   |     |             |
   |     |             expected `&i32`, found `i32`
   |     |             help: consider borrowing here: `&x`
   |     arguments to this function are incorrect
   |
note: function defined here
  --> src/main.rs:1:4
   |
1  | fn print_i32_ref(val: &i32) {
   |    ^^^^^^^^^^^^^ ---------

For more information about this error, try `rustc --explain E0308`.
error: could not compile `playground` due to previous error

So Rust indeed does make a difference between x and &x, but there are some places where this conversion happens magically. This can be a source of confusion.

You are right, the true reason why this works is that the Display trait is implemented for the reference to a Displayable type as well. Thank you for correcting me in that matter. I wasn't aware of it.

I made an example to demonstrate this with the Debug trait, which acts the same:

fn debug<T: std::fmt::Debug>(arg: T) {
    println!("Showing: {arg:?}");
}

fn main() {
    let v = vec![5, 5, 5];
    debug(&v); // this works because `&Vec<i32>` also implements `Debug`
    debug(v);
}

(Playground)

Output:

Showing: [5, 5, 5]
Showing: [5, 5, 5]

So this is the reason why println! can work on references (i.e. why you can omit the *), and the macro-magic is why you can omit the & and still not consume the value:

fn debug<T: std::fmt::Debug>(arg: T) {
    println!("Showing: {arg:?}");
}

fn main() {
    let v = vec![5, 5, 5];
    debug(&v); // this works because `&Vec<i32>` also implements `Debug`
    println!("{v:?}"); // we can call this again and again
    println!("{v:?}"); // we can call this again and again
    println!("{v:?}"); // we can call this again and again
    println!("{v:?}"); // we can call this again and again
    println!("{v:?}"); // we can call this again and again
    println!("{v:?}"); // we can call this again and again
    println!("{v:?}"); // we can call this again and again
    debug(v);
    //debug(v); // but we cannot call this again!
}

(Playground)

Output:

Showing: [5, 5, 5]
[5, 5, 5]
[5, 5, 5]
[5, 5, 5]
[5, 5, 5]
[5, 5, 5]
[5, 5, 5]
[5, 5, 5]
Showing: [5, 5, 5]

So the macro-magic is somewhat involved here, because println!("{x}") will not consume x.

1 Like

@jbe thanks for the clear answer with examples.

I was using the print to actually understand what each of the variables are storing.
I tried to print the val, &val, &&&val and all are de-referencing as many times as required to get to the value. The same happens when using the println!("{:?}", &&&&val) and with dbg!(&&&&val) also.

Is there any other option, that would print the variable as-is.
Note that this is only for debugging purpose.

If you need to print a pointer, ask for printing a pointer explicitly, like println!("{:p}", &val). That's the edge case in practice, so one have to acknowledge if that's really what they want.

1 Like

One way to find out the type of a variable is to make an assignment with a wrong type:

fn print_i32_ref(val: &i32) {
    let _: () = val;
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types
 --> src/lib.rs:2:17
  |
2 |     let _: () = val;
  |            --   ^^^ expected `()`, found `&i32`
  |            |
  |            expected due to this

For more information about this error, try `rustc --explain E0308`.
error: could not compile `playground` due to previous error

The underscore _ is a special name that will discard the result. But by writing ": ()" we denote that the type must be the unit type (), which is clearly wrong in our case. So the compiler tells us the right type of val, which is &i32.

1 Like

I would like to note that using :p formatting can be misleading when working with slices, because it will only print the base address, and not the length of a wide pointer:

fn main() {
    let v: Vec<i32> = vec![10, 20, 30];
    let s1: &Vec<i32> = &v;
    let s2: &[i32] = &v;
    let s3: &[i32] = &v[0..1];
    let s4: &[i32] = &v[0..2];
    println!("{s1:p} {s1:?}");
    println!("{s2:p} {s2:?}");
    println!("{s3:p} {s3:?}");
    println!("{s4:p} {s4:?}");
}

(Playground)

Output:

0x7ffd7a0c5a08 [10, 20, 30]
0x55c49d0599d0 [10, 20, 30]
0x55c49d0599d0 [10]
0x55c49d0599d0 [10, 20]

Note how s2, s3, and s4 are clearly distinct values/references, yet all print as 0x55c49d0599d0 in the example above.

1 Like