Hands-on Rust: Chapter 7 Invalid Camera Method from Chapter 5

In the end of chapter 7 we write the movement function in movement.rs
We call the camera on_player_move method like so:

camera​.on_player_move​(&want_move.destination);

This results in a compile error on my machine because on_player_move expects a reference.

The latest code in the book I see for the camera method is back in Chapter 5

 	    ​pub​ ​fn​ ​on_player_move​(&​mut​ ​self​, player_position: Point) {
​ 	        ​self​.left_x = player_position.x ​-​ DISPLAY_WIDTH​/​2;
​ 	        ​self​.right_x = player_position.x + DISPLAY_WIDTH​/​2;
​ 	        ​self​.top_y = player_position.y ​-​ DISPLAY_HEIGHT​/​2;
​ 	        ​self​.bottom_y = player_position.y + DISPLAY_HEIGHT​/​2;
​ 	    }

Looking at the code zip it appears that the function is always expecting a reference, though I got a bit lost on the folder names so it could be wrong there too.

Changing the method to expect a reference fixes the compile error and it appears to run fine.

I didn’t test it but I think the method could take in either a Point or an &Point and as long as the method is called the same it should be fine? If this is true, is this because destination always is a calculated value that doesn’t need to worry about the borrow checker?

2 Likes

Thanks for finding that - great catch! I’m working on clearing that one up now, so it should make beta 2. It looks like an error slipped in when I merged some code branches on my machine (the bundled source compiles because the on_player_move signature changed without being mentioned in the text).

It’ll work either way, so long as the function is either &Point or Point and the calling code follows the same convention. The correct form is just Point. The reasoning behind this is a little complicated (more so than I can justify adding into a beginner’s book), so I’ll try and explain it.

The Point structure (source) has a few properties:

  • It’s internal representation is two i32, 32-bit signed integers.
  • It derives Copy to make it “trivially copyable”.
  • It implements all manner of niceties to work with other libraries and use a crate named Ultraviolet under the hood for fast SIMD math when you use it as a (math-style) vector.

Since the type is two 32-bit integers, its memory representation is 64-bits long. That’s really helpful, because on 64-bit architectures (most PCs these days) it fits into a single register. Copying it to a function becomes a VERY fast operation - place it in a register and off it goes. Even without a register, copying a single 64-bit value is a ridiculously fast memcpy operation under the hood. The fun part is that sending a reference is actually slower in this case! References are really pointers under the hood. So the system sends a pointer to the value to the function. The pointer is 64-bits long, so the pointer is the exact same size. But the function then has to de-reference the pointer - lookup the memory address being pointed to, and get the value from there. So the computer has to perform an extra step to get to the value. There’s a Clippy warning in “pedantic” mode that will warn you about this sometimes.

When you compile in release mode, LLVM (the underlying compiler system) is often smart enough to transform a read-only pointer into a copy. It’s also smart enough to realize that if it puts the value into a register earlier and “inlines” the function call (literally copy it inline into the calling code) it can skip copying/passing altogether. It doesn’t always get that right, but it usually does. (“RVO - Return Value Optimization” can cause “copy elision” to happen, often eliminating the entire copy. It’s not guaranteed to happen in Rust, but it usually does)

So, that’s great - but there are times you want to be referencing Point rather than copying it. Every time you are getting one out of Legion, a reference is preferable because you are operating on the original value in Legion’s data store. If you are updating the point (with an &mut) you absolutely have to use a reference - otherwise your updates won’t apply to the original. When you are accessing a bunch of Point values, you tend to gain more from them being adjacent in cache than you lose from the pointer jump - a jump to something in L1 cache is effectively instant.

Hope that wasn’t too long an explanation. TLDR: it’s fixed for beta 2, thank you for spotting it.

2 Likes

Thanks that helps.

Would a function ever declare a mut parameter instead of &mut? Is that even possible or would ever make sense?
Feel free to tell me to pound sand if you don’t want to answer those types of questions here :slight_smile:

1 Like

That’s a fun one. :slight_smile: I’ve written a few functions with mutable, non-reference parameters - usually when porting directly from C++ and its ilk. It’s a good way to let the function modify the parameter without side-effects - that is, the changes to the parameter never leave the function.

Say you had a function:

fn do_stuff(mut n: i32) {
   while n > 0 {
      // Do something with n here!
      n -= 1;
   }
}

When you call do_stuff, it modifies n inside the function - but the original n that is passed in never changes. n is either a copy or a move depending upon the type - but it is now local to the function, just as if you’d typed let mut n = 5 as the first line of the function. So when you call it:

let mut n = 5;
do_stuff(n);
println!("{}", n);

You still get the output of 5, because n is copied (i32 copies by default - most primitives do). Whereas if you’d use a mutable reference, you’d get:

fn do_stuff(n: &mut i32) {
   while n > 0 {
      // Do something with n here!
      n -= 1;
   }
}

..
let mut n = 5;
do_stuff(&mut n);
println!("{}", n);

You will get the output 0, because the original n was modified.

I often think of mutable borrow parameters as “out” parameters (some other languages use that terminology) - because they are effectively returning something, even if they are doing it through a parameter rather than a return statement.

Hope that helps! (Sorry about the edits; I wrote it on my phone the first time, and Rust isn’t well suited to mobile keyboards)

1 Like