10

Today's Rust mystery is from section 4.9 of The Rust Programming Language, First Edition. The example of references and borrowing has this example:

fn main() {
    fn sum_vec(v: &Vec<i32>) -> i32 {
        return v.iter().fold(0, |a, &b| a + b);
    }

    fn foo(v1: &Vec<i32>) -> i32 {
        sum_vec(v1);
    }

    let v1 = vec![1, 2, 3];

    let answer = foo(&v1);
    println!("{}", answer);
}

That seems reasonable. It prints "6", which is what you'd expect if the v of sum_vec is a C++ reference; it's just a name for a memory location, the vector v1 we defined in main().

Then I replaced the body of sum_vec with this:

fn sum_vec(v: &Vec<i32>) -> i32 {
    return (*v).iter().fold(0, |a, &b| a + b);
}

It compiled and worked as expected. Okay, that's not… entirely crazy. The compiler is trying to make my life easier, I get that. Confusing, something that I have to memorize as a specific tic of the language, but not entirely crazy. Then I tried:

fn sum_vec(v: &Vec<i32>) -> i32 {
    return (**v).iter().fold(0, |a, &b| a + b);
}

It still worked! What the hell?

fn sum_vec(v: &Vec<i32>) -> i32 {
    return (***v).iter().fold(0, |a, &b| a + b);
}

type [i32] cannot be dereferenced. Oh, thank god, something that makes sense. But I would have expected that almost two iterations earlier!

References in Rust aren't C++ "names for another place in memory," but what are they? They're not pointers either, and the rules about them seem to be either esoteric or highly ad-hoc. What is happening such that a reference, a pointer, and a pointer-to-a-pointer all work equally well here?

turbulencetoo
  • 3,447
  • 1
  • 27
  • 50
Elf Sternberg
  • 16,129
  • 6
  • 60
  • 68

1 Answers1

10

The rules are not ad-hoc nor really esoteric. Inspect the type of v and it's various dereferences:

fn sum_vec(v: &Vec<i32>) {
    let () = v;
}

You'll get:

  1. v -> &std::vec::Vec<i32>
  2. *v -> std::vec::Vec<i32>
  3. **v -> [i32]

The first dereference you already understand. The second dereference is thanks to the Deref trait. Vec<T> dereferences to [T].

When performing method lookup, there's a straight-forward set of rules:

  1. If the type has the method, use it and exit the lookup.
  2. If a reference to the type has the method, use it and exit the lookup.
  3. If the type can be dereferenced, do so, then return to step 1.
  4. Else the lookup fails.

References in Rust aren't C++ "names for another place in memory,"

They absolutely are names for a place in memory. In fact, they compile down to the same C / C++ pointer you know.

Community
  • 1
  • 1
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • 1
    When you actually work out the method call by hand, it looks a bit like `let v = &vec![42]; <[_]>::iter(&**v);`. The sequence is: Reference to Vec, Vec, Slice, Reference to Slice. – Josh Lee Apr 05 '17 at 17:49
  • Thanks. Sorry for the newbie stuff; I promise I'm actually very good at C and Python. And I can't help but experiment when the textbook gives me an example. – Elf Sternberg Apr 05 '17 at 17:49
  • 4
    @ElfSternberg no worries; it's not really "newbie". In fact, I'd say that this particular question would require someone who understands what pointers / references are to be able to be confused by it. The main tricky thing here is the ergonomics that Rust provides and knowing what they do under the hood. – Shepmaster Apr 05 '17 at 18:42