3
votes

Reading the rust tutorial managed and owned pointers can be passed to a function requiring borrowed pointers as-is and are converted to borrowed pointers at compile time.

Why can't the stack variable be passed in the same way? What syntax or feature requires the explicit & operator to pass that one as opposed to the compiler automatically converting it?

struct Point {x: float, y: float}

let on_the_stack :  Point =  Point {x: 3.0, y: 4.0};
let managed_box  : @Point = @Point {x: 5.0, y: 1.0};
let owned_box    : ~Point = ~Point {x: 7.0, y: 9.0};

fn compute_distance(p1: &Point, p2: &Point) -> float {
    let x_d = p1.x - p2.x;
    let y_d = p1.y - p2.y;
    sqrt(x_d * x_d + y_d * y_d)
}

compute_distance(&on_the_stack, managed_box);
compute_distance(managed_box, owned_box);

Adding to the confusion is a quote from kibwen on ycombinator (I can't find the original quote but here's the quote of the quote)

We’ll still have & and &mut, but those aren’t pointers, they’re references (it’s our own fault for calling them “borrowed pointers” in our documentation)

If compute_distance is taking references and the compiler converts pointers to references automatically why can't it do the same for values?

Edit: Since pnkfelix seems to know what he's talking about I'll copy some dialogue here for easier reading.

pnkfelix

The designers of Rust decided to not follow the path of C++ in this respect. One side-effect of this decision is that when someone reading the code looks at an invocation like f(x, y) in Rust, one need not spend time wondering "wait, how does f take its arguments; if it mutates y, will I see that reflected in that after f returns? What about x?" etc.

J V

I may be confused on the nature of references then. @var and ~var in rust are pointers (Though they do behave more like references) - are C++ references simply pointers under the hood?

In any case, I could apply the same logic to C with regards to code readability.

Variables are either values or pointers (Where you simply pass the variable as you do in rust: f(var)) or are referenced in the function call (like you do in rust: f(&var)) - I would have thought the rust compiler would recognize the function signature and handle this automatically. I don't see the improvement over C or C++

pnkfelix

One followup: this line that I wrote: "wait, how does f take its arguments; if it mutates y, will I see that reflected in that after f returns? What about x?" is somewhat facetious, since even a call like f(&x, y) would not be able to modify x; it would have to be f(&mut x, y). (and the declaration of x itself would have to be let mut x = ..., etc. – pnkfelix 1 hour ago

2nd followup: a reason why explicit &x may be more important in Rust than in C/C++ (rather than letting f(x,y) implicitly do the borrow &x), is because the borrow checker forces borrows to follow certain rules, and will refuse to compile code that does not conform. When you get an error from the borrow-checker, IMO it is a better user-experience if the compiler points to an expression of the form &x or &mut x in the source, rather that pointing to a function call and saying "there's an implicit borrow here." (That's subjective opinion, of course.) – pnkfelix 1 hour ago

I saw that you had a follow-up note in your reply, but I don't understand the point you are making about applying "the same logic to C". If you mean C as opposed to C++, then you usually do have to explicitly take addresses of memory when invoking functions that expect pointers. If a function takes a int**, then someone needs to do &E where E is an l-value of type int*. That seems analogous to Rust to me. (The main exception to this that I recall offhand from C is function-pointers; you don't need to do &f to make a function pointer to f.) – pnkfelix 1 hour ago

J V

That seems analogous to Rust to me. - exactly my point - from a readability point of view there's not much of an improvement over plain C. because the borrow checker forces borrows to follow certain rules...rather that pointing to a function call and saying "there's an implicit borrow here." Bingo - this is one of the underlying reasons I was looking for. If you find any other reasons for the manual cast feel free to add to your answer!

1

1 Answers

4
votes

I think the intent is to make it clear to the reader when a copy of the struct is being made versus when a reference to the existing struct is being passed along. (more on this below.)

The managed-box and owned-box in the example listed are already references; they are just being temporarily converted from one kind of a reference (@Point and ~Point respectively) into &Point references. But they are both just references, nonetheless, and no copy is being made of the referenced storage.


It might well be the case that the Rust compiler could infer, based on the type signature of the function being called, when a reference needs to be created versus when the struct needs to be copied in.

Indeed, that's exactly what a C++ compiler does when it sees a call like f(x, y): it needs to look at the signature of f, such as f(A &a, B b), and from that determine: "Okay, I'll make a reference for the x argument (assuming it is of type A) and make a copy of the y argument (assuming it is of type B), since it is passed-by-value."

The designers of Rust decided to not follow the path of C++ in this respect. One side-effect of this decision is that when someone reading the code looks at an invocation like f(x, y) in Rust, one need not spend time wondering "wait, how does f take its arguments; if it mutates y, will I see that reflected in that after f returns? What about x?" etc.

  • (Of course, one still needs to reason about memory reachable from x and y via any mutable or owned references they themselves hold... but I digress.)