rust

Using `&` several times does not cause a compilation error


Consider the following code for reading a file:

use std::{fs::File, io::Read};
use std::any::type_name_of_val;

fn main() {
    let path: &str = "foo.txt";
    let mut file: File = match File::open(&&&path) {
        Ok(f) => f,
        Err(reason) => panic!("Could not open file: {path}, reason: {reason}"),
    };

    let mut data: String = String::new();
    match file.read_to_string(&mut data) {
        Ok(size) => println!("Read {size} bytes, content {data}"),
        Err(reason) => panic!("Could not read file: {path}, reason: {reason}"),
    }

    println!("file {:?}", path);     // These two lines return the same value: "foo.txt"
    println!("file {:?}\n", &path);  // even when this line is a reference to &str

    println!("type {:?}", type_name_of_val(path));
    println!("type {:?}", type_name_of_val(&path));
    println!("type {:?}", type_name_of_val(&&path));
    println!("type {:?}", type_name_of_val(&&&path));
}

Output:

> cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/read_file`
Read 35 bytes, content line 1
line 2
line 3
line 4
line 5

file "foo.txt"
file "foo.txt"

type "str"
type "&str"
type "&&str"
type "&&&str"

Question Why is this line valid?

File::open(&&&path)

I tried using File::open(path) (I presumed this is the right approach), but I noticed that using & multiple times still works.

I found this answer, but not sure if it applies to my example:

The compiler is able to dereference the argument of push_str as much as necessary, so s1.push_str(s2) works just as well as s1.push_str(&&&&&s2).


Solution

  • Creating a reference to a value is always valid. Whether that value is temporary or itself a reference. So &&&expr will construct a reference to a reference to a reference to the value of expr. So that alone won't cause a compilation error.

    There are two main ways that &T, &&T, &&&T, etc may all behave the same:

    1. Recursive Trait Implementations

      In your case with File::open, it will accept any value that implements AsRef<Path>. If we look for generic implementations on AsRef, you'll see it is implemented for references if the referenced type also implements AsRef. And since in the case of &&&path, that next type is also a reference and so on until it gets to the AsRef<Path> for str implementation.

      This is a fairly common pattern on traits (if able) in the standard library and even in third-party if seeking a good degree of generality. You also see it in action with the println! since Debug is also for &T where T: Debug.

    2. Deref Coercion

      This doesn't appear in your code but an example like this also works:

      fn handle_data(data: &[u8]) {
          // do something with data
      }
      
      fn main() {
          let data = vec![1, 2, 3];
          handle_data(&&&data);
      }
      

      This works because Rust will perform an implicit coercion from &T to &U if T implements Deref. Since references (tautologically) implement Deref, that has the same outcome of collapsing multiple &&&s into one. This also integrates with more types like above &Vec<u8> is coerced to &[u8] just like a &String coerces to a &str.

      Deref coercion isn't always applied - crucially it only works when the compiler knows the target type - but it is very common.