Rust, ownership and println()
If you’re new to Rust and playing around with println()
as a way of trying to understand ownership, this post could be useful for you.
The key takeaway is short, sweet and in TL;DR. The journey I took to get there makes up the rest of this post:
- Trying to break my code with an ownership error (and failing)
- Investigating
- Understanding
- Trying to break my code with an ownership error (and succeeding)
- Takeaways
- TL;DR
Trying to break my code with an ownership error (and failing)
I ended my last post confused about why my code was compiling - according to my understanding of Rust’s ownership model, the code I wrote should have produced an ownership error.
This was how the code looked before I tried to break it:
fn main(){
let cell_one: Cell = Cell {
row: 1,
column: 1,
north: None::<Box<Cell>>,
east: None::<Box<Cell>>,
south: None::<Box<Cell>>,
west: None::<Box<Cell>>
};
println!(" cell_one \n {:#?}", &cell_one); // <- reference, &cell_one
let cell_two: Cell = Cell{
row: 0,
column: 0,
north: Some(Box::new(cell_one)), // <- use directly, cell_one
east: None::<Box<Cell>>,
south: None::<Box<Cell>>,
west: None::<Box<Cell>>
};
println!(" cell_two \n {:#?}", cell_two);
}
This compiles successfully and on execution you get to see the two cells output to the terminal.
You can see that I’m passing a reference to cell_one
into println()
, using the &cell_one
syntax. I had thought that this was necessary because otherwise cell_two
couldn’t use cell_one
as one of its fields, because println()
would have taken ownership.
To test this understanding, I thought I could break the above working code and produce an ownership error by giving println()
the actual cell_one
, not just a reference.
Here’s what that looked like:
fn main(){
let cell_one: Cell = Cell {
row: 1,
column: 1,
north: None::<Box<Cell>>,
east: None::<Box<Cell>>,
south: None::<Box<Cell>>,
west: None::<Box<Cell>>
};
println!(" cell_one \n {:#?}", cell_one); // <- use directly, give ownership of cell_one to println!()
let cell_two: Cell = Cell{
row: 0,
column: 0,
north: Some(Box::new(cell_one)), // <- meaning it's unavailable here?
east: None::<Box<Cell>>,
south: None::<Box<Cell>>,
west: None::<Box<Cell>>
};
println!(" cell_two \n {:#?}", cell_two);
}
But, the code still compiled, I didn’t get the ownership error I wanted : (
To recap, I thought that it would not compile because…
- ownership of
cell_one
would be transferred toprintln()
cell_two
would try to usecell_one
for itsnorth
field- but it would be unable to, because
cell_one
would be owned byprintln()
Investigating
So, what was wrong about my understanding?
I am very suspicious of new things, and Box::new
was no exception. So I initially thought that this was introducing an as-yet-unknown consideration to how ownership worked.
But before I dug into that, I thought it best to re-establish the basics of ownership. I re-read the introductory documentation, focusing on how functions take ownership of what is passed into them. This confirmed my initial understanding: if you pass a variable directly (without referencing or copying etc.) into a function, that function will take ownership.
On realising that my mental model was broadly correct, and println()
was an exception, I pulled on that thread, putting all my hours of art and art history writing into crafting the search term ‘Rust does println take ownership?’.
The five year MFA paid off, and Google gave me the information I needed: println()
silently takes a reference by default! 1 . Even when you pass in a value directly, the internals of println()
will only use a reference to what you gave it. Box::new
is innocent! 2
Understanding
To summarise, the code that I didn’t want to compile was compiling because…
- the ownership of
cell_one
was not transferred toprintln!()
cell_two
would try to usecell_one
for itsnorth
field- and it would be able to, because
println!()
only ever took a reference ofcell_one
(As an aside, with this new knowledge the use of &
when feeding &cell_one
to println()
in the first code snippet was redundant).
Trying to break my code with an ownership error (and succeeding)
Knowing println()
couldn’t give me the error message I wanted, I had to look elsewhere - dbg()
seemed like it would do the trick:
fn main(){
let cell_one: Cell = Cell {
row: 1,
column: 1,
north: None::<Box<Cell>>,
east: None::<Box<Cell>>,
south: None::<Box<Cell>>,
west: None::<Box<Cell>>
};
dbg!("cell_one \n {:#?}", cell_one); //ownership actually transferred
let cell_two: Cell = Cell{
row: 0,
column: 0,
north: Some(Box::new(cell_one)),
east: None::<Box<Cell>>,
south: None::<Box<Cell>>,
west: None::<Box<Cell>>
};
println!(" cell_two \n {:#?}", cell_two);
}
Yes!
dbg!()
has delivered the goods, giving me all the E0382 ownership error I could have ever dreamed of:
error[E0382]: use of moved value: `cell_one`
--> src/cell.rs:25:30
|
12 | let cell_one: Cell = Cell {
| -------- move occurs because `cell_one` has type `Cell`, which does not implement the `Copy` trait
...
20 | dbg!("cell_one \n {:#?}", cell_one);
| ----------------------------------- value moved here
...
25 | north: Some(Box::new(cell_one)),
| ^^^^^^^^ value used here after move
Takeaways
- Always read the error message and the documentation thoroughly first.
- Then re-read them.
- Maybe do that again, just to be sure.
- Odds are on that the issue you’re facing has been faced by others before and is probably something simple.
- Just because you’re ignorant and fearful of something, don’t jump to conclusions that it’s the cause of whatever issues you’re having.
- Ownership is challenging but also very intriguing and for some reason quite rewarding.
TL;DR
Keep an eye out for using println()
when you're learning about ownership as it doesn’t take ownership of variables that are passed into it, it only ever uses a reference to them.
‘silently’ is not technically true, but ‘borderline inaudible’ is pretty fair: It’s not mentioned in the
println
documentation, and is mentioned only in passing in an unrelated chapter of the book, Chapter 5.2: An Example Program Using Structs where it says: "Another way to print out a value using the Debug format is to use the dbg! macro , which takes ownership of an expression (as opposed to println!, which takes a reference)…". It was only thanks to this SO post here that I was able to put these together.↩For now… 👀↩