Understanding references


#1

Disclaimer: I don’t have a background in systems programming.

I’m working on a small pet project to try to learn Rust. Three lines in I hit an error about the use of a moved value so I start throwing ampersands around till it works. Reading descriptions of what certain functions need I’m at a loss for figuring out why one grab-bag of ampersands placates the compiler when another doesn’t.

Maybe there is another dimension to this that I’m not seeing that gets swallowed up by type inference or something.

Anyways, here’s what I first wrote that broke:

use std::fs;
use std::path::Path;

fn main() {
    let paths = fs::read_dir(Path::new(".")).unwrap();

    for path in paths {
        let p = path.unwrap().path();
        if fs::metadata(p).unwrap().is_dir() {
            println!("totally a dir: {}", p.display())
        }
    }
}

Here’s the error:

$ cargo run
   Compiling ug v0.1.0 (file:///Users/conrad/dev/ug)
src/main.rs:10:43: 10:44 error: use of moved value: `p` [E0382]
src/main.rs:10             println!("totally a dir: {}", p.display())
                                                         ^
note: in expansion of format_args!
<std macros>:2:25: 2:56 note: expansion site
<std macros>:1:1: 2:62 note: in expansion of print!
<std macros>:3:1: 3:54 note: expansion site
<std macros>:1:1: 3:58 note: in expansion of println!
src/main.rs:10:13: 10:55 note: expansion site
note: in expansion of for loop expansion
src/main.rs:7:5: 12:6 note: expansion site
src/main.rs:10:43: 10:44 help: run `rustc --explain E0382` to see a detailed explanation
src/main.rs:9:25: 9:26 note: `p` moved here because it has type `std::path::PathBuf`, which is non-copyable
src/main.rs:9         if fs::metadata(p).unwrap().is_dir() {
                                      ^
note: in expansion of for loop expansion
src/main.rs:7:5: 12:6 note: expansion site
error: aborting due to previous error
Could not compile `ug`.

To learn more, run the command again with --verbose.

I fixed it by just tacking ampersands to places where the path is used in the body of the loop:

use std::fs;
use std::path::Path;

fn main() {
    let paths = fs::read_dir(Path::new(".")).unwrap();

    for path in paths {
        let p = path.unwrap().path();
        if fs::metadata(&p).unwrap().is_dir() {
            println!("totally a dir: {}", &p.display())
        }
    }
}

BUT it can also be fixed by adding a ref keyword to that let p binding:

use std::fs;
use std::path::Path;

fn main() {
    let paths = fs::read_dir(Path::new(".")).unwrap();

    for path in paths {
        let ref p = path.unwrap().path();
        if fs::metadata(p).unwrap().is_dir() {
            println!("totally a dir: {}", p.display())
        }
    }
}

I have a bunch of other questions about various permutations, but let’s start here for now. Are these two blocks of code equivalent? I am only able to see that they get me my desired result, but I want to know what is going on here for sure because I know if my mental model of what’s going on in Rust is wrong it will make using this impossible.


#2

Sort of.

To understand the problem, let’s look at the signature for fs::metadata: http://doc.rust-lang.org/stable/std/fs/fn.metadata.html

pub fn metadata<P: AsRef<Path>>(path: P) -> Result<Metadata>

This function takes something that can be turned into a reference to a Path. It takes ownership of this thing. So when you pass in p, it gets moved, hence your original error. In your second and third versions, you pass in a reference to the Path, and since it’s just a reference, the original doesn’t get consumed.

Your question about the two samples really boils down to this: what’s the difference between

let ref p = ...

and

let p = ... ; &p

ref is described here: http://doc.rust-lang.org/stable/book/patterns.html#ref-and-ref-mut

TL:DR: they’re the same thing.


#3

The effect is the same, yes. But they differ in what you could do later with p if you extended the code.

In the let ref p = somepathbuf case, you only ever have a reference (type of p is &PathBuf). You can’t later move out the PathBuf to another function, which you could do in the other case as soon as the borrow is out of scope.

By the way: the & in &p.display() is not doing what you think it does; it creates a reference to the result of display(). If you wanted to take a reference to p you’d have to write (&p).display() - however, that is made unnecessary since method calls already create self-references as necessary.


#5

Thanks for describing how they’re different – namely by using & later would let me defer when I want to use a reference to the PathBuf.

Regarding (&p).display(), the creation of the reference is redundant because the signature of the function is display(&self) -> Display ?

So in the function declaration, that means that a reference gets created when you call the function, rather than that function requiring a reference. This is different from fs::metadata 's signature:

pub fn metadata<P: AsRef<Path>>(path: P) -> Result<Metadata>

So if fs::metadata wants a reference, why can’t it just have a signature like:

pub fn metadata<P: &Path>(path: P) -> Result<Metadata>

or maybe

pub fn metadata<P: Path>(&path: P) -> Result<Metadata>

to create the reference in the same way?

I am probably missing a huge bit about why ‘moving’ things and transfering ownership helps you out when you write code…


#6

The signature would be

pub fn metadata<P: Path>(path: &P) -> Result<Metadata>

if it just took a reference to a Path, but by using AsRef<Path>, it can be anything that can be converted to a reference to a path. Subtly different.


#7
I am probably missing a huge bit about why 'moving' things and transfering ownership helps you out when you write code...

It helps by making the library the most flexible it can be. As a wise man once said, “Be liberal in what you accept”. So it’s up to you to decide what helps you the most.


#8

Ah, gotcha. Thanks!