Lifetimes, References and Borrowing

When you assign an object to a variable in Rust, you are said to be binding it. i.e your variable “owns” the object for as long as it is in scope and when it goes out of scope it is destroyed.

  1. {
  2. let v1 = vec![1, 2, 3, 4]; // Vec is created
  3. ...
  4. } // v1 goes out of scope, Vec is dropped

So variables are scoped and the scope is the constraint that affects their lifetime. Outside of the scope, the variable is invalid.

In this example, it is important to remember the Vec is on the stack but the pointer it allocates to hold its elements is on the heap. The heap space will also be recovered when the Vec is dropped.

If we assign v1 to another variable, then all the object ownership is moved to that other variable:

  1. {
  2. let v1 = vec![1, 2, 3, 4];
  3. let v2 = v1;
  4. ...
  5. println!("v1 = {:?}", v1); // Error!
  6. }

This may seem weird but it’s worth remembering a serious problem we saw in C++, that of copy constructor errors. It is too easy to duplicate a class and inadvertantly share private date or state across multiple instances.

We don’t want objects v1 and v2 to share internal state and in Rust they don’t. Rust moves the data from v1 to v2 and marks v1 as invalid. If you attempt to reference v1 any more in your code, it will generate a compile error. This compile error will indicates that ownership was moved to v2.

Likewise, if we pass the value to a function then that also moves ownership:

  1. {
  2. let v1 = vec![1, 2, 3, 4];
  3. we_own_it(v1);
  4. println!("v = {:?}", v1);
  5. }
  6. fn we_own_it(v: Vec<i32>) {
  7. // ...
  8. }

When we call we_own_it() we moved ownership of the object to the function and it never came back.
Therefore the following call using v1 is invalid. We could call a variation of the function called we_own_and_return_it() which does return ownership:

  1. v1 = we_own_and_return_it(v1)
  2. ...
  3. fn we_own_and_return_it(v: Vec<i32>) -> Vec<i32> {
  4. // ...
  5. v1
  6. }

But that’s pretty messy and there is a better way described in the next section called borrowing.

These move assignments look weird but it is Rust protecting you from the kinds of copy constructor error that is all too common in C++. If you assign a non-Copyable object from one variable to another you move ownership and the old variable is invalid.

If you truly want to copy the object from one variable to another so that both hold independent objects you must make your object implement the Copy trait. Normally it’s better to implement the Clone trait which works in a similar way but through an explicit clone() operation.

Variables must be bound to something

Another point. Variables must be bound to something. You cannot use a variable if it hasn’t been initialized with a value of some kind:

  1. let x: i32;
  2. println!("The value of x is {}", x);

It is quite valid in C++ to declare variable and do nothing with it. Or conditionally do something to the variable which confuses the compiler so it only generates a warning.

  1. int result;
  2. {
  3. // The scope is to control the lifetime of a lock
  4. lock_guard<mutex> guard(data_mutex);
  5. result = do_something();
  6. }
  7. if (result == 0) {
  8. debug("result succeeded");
  9. }

The Rust compiler will throw an error, not a warning, if variables are uninitialised. It will also warn you if you declare a variable and end up not using it.

References and Borrowing

We’ve seen that ownership of an object is tracked by the compiler. If you assign one variable to another, ownership of the object is said to have moved to the assignee. The original variable is invalid and the compiler will generate errors if it is used.

Unfortunately this extends to passing values into functions and this is a nuisance.
But variable bindings can be borrowed. If you wish to loan a variable to a function for its duration, you can pass a reference to the object:

  1. {
  2. let mut v = Vec::new(); // empty vector
  3. fill_vector(&mut v);
  4. // ...
  5. println!("Vector contains {:?}", v);
  6. }
  7. //...
  8. fn fill_vector(v: &mut Vec<i32>) {
  9. v.push(1);
  10. v.push(2);
  11. v.push(3);
  12. }

Here we create an empty vector and pass a mutable reference to it to a function called fill_vector(). The compiler knows that the function is borrowing v and then ownership is returned to v after the function returns.