Yep

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:

  1. Trying to break my code with an ownership error (and failing)
  2. Investigating
  3. Understanding
  4. Trying to break my code with an ownership error (and succeeding)
  5. Takeaways
  6. 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…

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…

(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

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.


  1. ‘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.

  2. For now… 👀