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 
1 Like
That’s a fun one.
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