storojs72
storojs72

Reputation: 185

Clarifying Rust's ownership rules

I'm reading Rust Programming Language book and would like to clarify following Listing 4.5 in Return Values and Scope section:

fn main() {
   let s1 = String::from("hello");
   let (s2, len) = calculate_length(s1);
   println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
   let length = s.len(); // len() returns the length of a String
   (s, length)
}

If I use:

fn calculate_length(s: String) -> (String, usize) {
   (s, s.len())
}

then I'm getting:

error[E0382]: borrow of moved value: `s`
  --> guessing_game/src/main.rs:46:9
   |
43 | fn calculate_length(s: String) -> (String, usize) {
   |                     - move occurs because `s` has type `String`, which does not  implement the `Copy` trait
...
46 |     (s, s.len())
   |      -  ^ value borrowed here after move
   |      |
   |      value moved here

At the same time if I swap values in returning tuple to (s.len(), s) (in calculate_length function) and rewrite main caller accordingly, it compiles without errors.

Intuitively I understand this borrowing error (and how to fix it), but could somebody clarify/explain this behavior "more formally"?

Upvotes: 0

Views: 150

Answers (1)

Kevin Reid
Kevin Reid

Reputation: 43743

since this construction seems to be an "atomic" operation

Constructing a tuple is “atomic”, but evaluating an expression is not. In general, expressions can contain arbitrary side effects in any part of them, so the order of evaluation of sub-expressions can affect the semantics of the program. Some languages leave it partly unspecified, but in Rust, the evaluation order is always left-to-right.

So, evaluating (s, s.len()) consists of the following steps:

  1. Evaluate the first subexpression, s. Since s is a type that is not Copy, this moves the value out of the variable s.
  2. Evaluate the second subexpression, s.len(). (This is an error since s is already moved.)
  3. Construct the tuple from the value of the first subexpression and the value of the second subexpression.

In this case, looking at the program we can see that it would not have any undesirable consequences to reorder the two subexpression evaluations, but Rust's evaluation order does not have any special cases for different types of expressions.

(The optimizer will perform lots of transformations on this code, in particular making nearly all “moves” not involve any memory copying or other actual CPU operations, but it would be very hard to program Rust if the validity of a program depended on the optimizer's choices.)

Upvotes: 2

Related Questions