Skip to content

Printing borrows: why println! never moves

Level: 101 · for newcomers

One line: println! is handed &s, never s, and so is every other formatting macro — so a String can be printed as often as you like, and the move error only comes from something that takes the String by value.

fn main() {
    let s = "foo".to_string();   // or String::from("foo"): the same String
    println!("{s}");             // foo
    println!("{s}");             // foo
}

s is a String, and a String moves rather than copies. There is no E0382 here because nothing moves s: both lines only read it.


The trap: reading println! as a function call

println!("{s}") looks like a call to a function named println with s as its argument, and a function with a String parameter would take s away from you. The ! says it is a macro ↗: it is replaced by other code before the borrow checker runs, and that code decides how s is used.

What it turns into

rustc will print that code — Printing the HIR takes the command apart. -Zunpretty is a nightly flag, and RUSTC_BOOTSTRAP=1 makes the stable compiler accept it — fine for looking, not for building:

RUSTC_BOOTSTRAP=1 rustc --edition 2024 -Zunpretty=hir main.rs

For let s = String::from("foo"); println!("{s}"); on 1.98.0:

Abridged — the prelude lines above main are dropped
fn main() {
    let s = String::from("foo");
    {
        ::std::io::_print({
                super let args = (&s,);
                super let args = [format_argument::new_display(args.0)];
                unsafe { format_arguments::new(b"\xc0\x01\n\x00", &args) }
            });
    };
}

(&s,) is the line to read: the macro builds a tuple of references to its arguments, and everything after it works through those. The rest — super let, format_argument, the byte string — is compiler-internal and changes between releases. -Zunpretty=expanded stops one step earlier, at format_args!("{0}\n", s), where the & is not visible yet.

The compiler calls it an immutable borrow

A mutable borrow that is still in use blocks println!:

fn main() {
    let mut s = String::from("foo");
    let r = &mut s;
    // println!("{s}");   // error[E0502] — transcript below
    r.push('!');
    println!("{s}");      // foo!
}

With that line uncommented:

error[E0502]: cannot borrow `s` as immutable because it is also borrowed as mutable
 --> main.rs:4:16
  |
3 |     let r = &mut s;
  |             ------ mutable borrow occurs here
4 |     println!("{s}");   // error[E0502] — transcript below
  |                ^ immutable borrow occurs here
5 |     r.push('!');
  |     - mutable borrow later used here

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0502`.

The caret sits under {s}, labelled immutable borrow occurs here. Put let t = s; on that line instead — a real move — and the error is E0505, cannot move out of s because it is borrowed.

Every formatting macro borrows

The example prints a Ticket that is neither Copy nor Clone and prints a line when it is dropped, and uses it again after each of these:

Macro What it does with its arguments
println! ↗, print! ↗ borrows
format! ↗ borrows, returns a new String
write! ↗ borrows, writes into the buffer you pass
assert! ↗ with a message borrows, formats the message only on failure
panic! ↗ borrows — checked even in a branch that never runs
assert_eq! ↗ borrows both sides

The ticket's drop line does not appear until something takes it by value.

What does move

Something that takes the value itself. Three shapes you will meet.

A by-value parameter — fn take(t: Ticket), and likewise let t = s;, drop(s) and vec![s]. In the output, take(t) prints ~ ticket #7 dropped before control is back in main.

An argument expression that consumes. The macro borrows the result of each argument, so if computing the argument moves s, the move is over before println! gets there:

fn main() {
    let s = String::from("foo");
    println!("{}", s + "!");   // foo! — the + takes s by value
    // println!("{s}");        // error[E0382] — transcript below
}
Abridged — a note pointing into the toolchain's own source file is dropped
error[E0382]: borrow of moved value: `s`
  --> main.rs:4:16
   |
 2 |     let s = String::from("foo");
   |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
 3 |     println!("{}", s + "!");   // foo! — the + takes s by value
   |                    ------- `s` moved due to usage in operator
 4 |     println!("{s}");        // error[E0382] — transcript below
   |                ^ value borrowed here after move
   |
help: consider cloning the value if the performance cost is acceptable
   |
 3 |     println!("{}", s.clone() + "!");   // foo! — the + takes s by value
   |                     ++++++++

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0382`.

The example makes the order visible: label(u) takes a ticket by value, and its drop line is printed before the line println! was asked to print.

dbg!, the printing macro that takes its argument by value. It hands the value back so it can wrap an expression, which means a bare dbg!(s); moves s and drops it at the semicolon:

fn main() {
    let s = String::from("foo");
    dbg!(s);              // moves s, prints to stderr, drops it
    // println!("{s}");   // error[E0382] — transcript below
}
error[E0382]: borrow of moved value: `s`
 --> main.rs:4:16
  |
2 |     let s = String::from("foo");
  |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
3 |     dbg!(s);              // moves s, prints to stderr, drops it
  |     ------- value moved here
4 |     println!("{s}");   // error[E0382] — transcript below
  |                ^ value borrowed here after move
  |
help: consider borrowing instead of transferring ownership
  |
3 |     dbg!(&s);              // moves s, prints to stderr, drops it
  |          +

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0382`.

The help line is the habit: dbg!(&s). What dbg! does has the rest.

"foo".to_string() or String::from("foo")?

The same String — the example builds one each way and prints a == b: true — so neither spelling changes anything above. Which to prefer is style, argued and measured on Making a String and ToOwned.

If you are coming from another language

  • Python. print(s) passes a reference to the object, and nothing in Python is ever moved, so the question never comes up. Rust agrees for println!, which is handed a reference. The half Python has no counterpart for is the function: def take(s) shares the object with its caller, while Rust's fn take(s: String) takes it, and the caller's s is unusable afterwards.
  • C. printf("%s", s) passes a copy of the pointer and reads the text through it — the same shape as println! holding &s. What changed is the checking: printf trusts that %s meets a char * still pointing at live memory, while format_args! checks each argument's type at compile time and the borrow checker checks the value is still alive.
  • C++. std::cout << s binds a const std::string&, a borrow in all but name. The difference is the by-value parameter: void take(std::string s) copies, leaving the caller's s intact, and after take(std::move(s)) a later use of s still compiles, against a string in a valid but unspecified state. Rust's take(s) moves by default, and the later use is E0382.

The verified output

Verified output of printing_borrows.rs — regenerated by tools/run_examples.py, never hand-typed.

──── The two spellings from the question
  foo foo
  foo foo
  a == b: true

──── Every formatting macro borrows: t is used again after each one
  println!   ticket #7
  print!     ticket #7
  format!    ticket #7
  write!     ticket #7
  assert!    passed, so its message was never formatted
  panic!     compiled, in a branch that did not run
  assert_eq! passed, and a and b are still here: foo foo
  no drop line yet: t still owns ticket #7

──── A function that takes it by value: the move
  take() owns ticket #7 now
  ~ ticket #7 dropped
  back in main; t was moved, so this line cannot name it

──── An argument that consumes: the move happens before println! runs
  ~ ticket #8 dropped
  ticket #8, labelled
  the drop line came first: label(u) returned before the print began

Run it yourself:

rustc --edition 2024 18_Ownership/printing_borrows/examples/printing_borrows.rs -o /tmp/pb && /tmp/pb

See also

Documentation

Po polsku

Pytanie, od którego zaczyna się ta strona: dlaczego dwa wywołania println!("{s}") na tym samym String nie dają błędu o przeniesionej wartości? Bo println! to makro, a nie funkcja, i każde makro formatujące — print!, format!, write!, panic!, assert_eq! — pożycza swoje argumenty. Rozwija się do kodu, który bierze &s, a nie s, więc wartość zostaje u właściciela i można ją drukować dowolnie wiele razy. Kompilator mówi to wprost: przy aktywnej referencji mutowalnej println! daje E0502, błąd o pożyczeniu, a nie o przeniesieniu.

Przeniesienie własności robi dopiero coś, co przyjmuje wartość: parametr typu String, let t = s;, drop(s), vec![s], argument w rodzaju s + "!" — oraz jedno makro, dbg!, które zabiera wartość i ją zwraca. Stąd nawyk dbg!(&s).

"foo".to_string() i String::from("foo") dają ten sam String; wybór jest kwestią stylu i niczego na tej stronie nie zmienia.

Szukaj po polsku: czy println! pożycza zmienną · makra formatujące w Ruscie · format_args! · rust println borrow or move · E0382 borrow of moved value