I’m going to demonstrate the situation where I most often have friction with The Borrow Checker and demonstrate a few patterns I use to make my code compile.

Let’s pretend we’re trying to connect to an imaginary Postgres database server using a connection pool. The API for this imaginary library requires us to first initialize a connection Pool with the connection string. Once the pool is initialized, we call connect() on it to get access to a usable, owned Connection for queries.

Why Returning References Can Be Difficult

Let’s start with a naive approach that won’t even compile:

// This won't compile!
fn connect(url: &str) -> &Connection {
    let pool: Pool = postgres::connection_pool::new(url);
    let connection: Connection = pool.connect();
    connection.set_database("startup.io");
    &connection
}

The compiler will complain about the lifetime of &Connection not lasting long enough. Why? The compiler wants to deallocate the Connection object at the end of the function, but it knows that the &Connection reference we’re trying to return points to that struct.

The Book does a great job of explaining owned values, borrowing, and lifetimes; but I’ll provide my own version here. I’m doing a lot of generalizing and glossing over complexities in the next few paragraphs. If you understand why my sample code won’t compile, feel free to skip ahead.

A Digression into Memory Allocation

The operating system acts as a banker lending out memory to running processes on the machine and reclaiming the memory when a process stops. Processes can also give back memory at any point while they are running. We call it “allocating” memory when a process asks the operating system for memory, and we call it “deallocating” memory when the process returns memory to the operating system. The operating system’s job is to ensure that it only allocates available, unused memory to processes.

Inside our program code, declaring a variable allocates memory. In the simplest case, the newly allocated memory stores the data we assign to the variable. For other kinds of data, we store the memory address of another variable. We call this second kind of variable a “pointer” because it “points” to other memory. (I’ll also use the term “reference” interchangeably with “pointer”.) Pointers exist mostly to (1) avoid duplicating data in memory and (2) provide predictable ways to pass unpredicatble amounts of data around our program.

A Brief Digression on Garbage Collectors

Many languages including Java and Python use a “garbage collector” to decide when a process should free memory. Every so often, the garbage collector will (1) interrupt whatever the programmer’s code is doing, (2) hunt for memory that doesn’t have pointers to it, (3) and free that memory. Garbage collectors operate at “runtime,” while the program is running. This means that programmers can let the garbage collector figure out when to free memory automatically, which makes garbage collected languages easier and safer but sometimes slower.

Some languages don’t use a garbage collector. In C, the programmer must decide when to free up memory manually, which is difficult to do correctly. If the programmer tries to use a pointer after the memory has been freed, Bad Things can happen – things like crashing the program or allowing a hacker to gain root access. What makes Rust unique is that although it doesn’t use a garbage collector, it still guarrantees that pointers will be safe to use.

How Rust Achieves Memory Safety without a Garbage Collector

The Rust compiler uses the metaphor of home “ownership.” If memory is a house, then exactly one variable is the owner of that house and the death of that owner triggers an estate sale (aka deallocation). A variable owns memory when the programmer assigns data to it. When the owner goes out of scope and dies, that memory is deallocated. Because the compiler always knows when a variable goes out of scope, it always knows when to deallocate memory at compile time so Rust programs don’t need to pause and garbage collect while running.

As in real estate, ownership can be transferred. Like selling a house, assigning one variable to another variable transers ownership. We say that the value has been “moved.” Similar to how selling my home means moving my stuff, moves in Rust copy data to a new place in memory. If a large amount of data is moved, we’re probably wasting RAM since the data must exist in at least two locations in memory.

To prevent waste, Rust also allows “borrowing” of memory. The programmer borrows memory by creating a pointer to it. If memory is a house and one variable is the owner, pointers are the renters who temporarily use that memory. If the owner goes out of scope and dies, pointers can no longer legally use that memory. The Rust compiler acts like a property management agency by tracking all ownership lifetimes and borrowing to make sure that no memory is being squatted on illegally. This aspect of the Rust compiler is known as The Borrow Checker, and it’s the thing which makes Rust uniquely Rust.

Back to the code sample

// This won't compile!
fn connect(url: &str) -> &Connection {
    let pool: Pool = postgres::connection_pool::new(url);
    let connection: Connection = pool.connect();
    connection.set_database("startup.io");
    &connection
}

Going back to our example, there’s only one scope involved: the entire body of fn connect() {..}. So, at the end of connect(), all variables declared inside the function will be deallocated. Invoking pool.connect() creates an unnamed Connection object in this scope, but because that object is created inside of the connect() scope, it also must be deallocated at the end of function. This means that after we return the pointer to the Connection, there won’t be a Connection anymore! This is what the compiler is trying to tell us: that pointer will refer to freed memory after the end of the function.

Patterns for Returning References

Pattern 1: Return Owned Values

Give up on references, and just return a full copy of the value. Depending on the use case, this might be a good solution, and it’s often the easiest way to appease The Borrow Checker.

fn connect(url: &str) -> Connection {
    let pool: Pool = postgres::connection_pool::new(url);
    let connection: Connection = pool.connect();
    connection.set_database("startup.io");
    connection
}

There are two changes needed in this trivial case:

  • Remove the &.. in the return value and use .. instead
  • Remove the & in the function body

Some types, like str and Path are only intended to be used with references, and they have a sibling type which is intended to be used only as an owned value. For these types, if we just try removing the & from our code, we’ll still get compile errors. Here’s an example that won’t compile:

// This won't compile!
fn returns_a_path() -> Path {
    let path: Path = somehow_makes_path();
    path
}

For these types which can only be used as references, look for an impl of the trait ToOwned. How it works is ToOwned consumes a shared reference and copies the value into a new owned reference. Here’s an example of leveraging the ToOwned trait:

fn returns_a_pathbuf() -> PathBuf {
    let path: &Path = somehow_makes_path();
    let pathbuf: PathBuf = path.to_owned();
    pathbuf
}

Pros:

  • Low Effort - generally, converting a return value from a shared reference to an owned value is really easy. I can usually get something to compile quickly and refactor later using this technique.
  • Widely Applicable - almost anywhere we can return a reference, we could also return an owned copy of that value
  • Safe - a new copy of the value cannot corrupt memory elsewhere, even when moving the data between threads

Cons:

  • Memory - for any nontrivial type, we’ll almost certainly be wasting memory by making identical copies of some piece of data
  • Synchronization - this value won’t be changed if we change the original value.

Using this technique is generally a bit of a kludge, and it should be a clue to me that it might be worth refactoring. In particular, if the value is something potentially large (like the contents of a file, unbounded user input, or something larger than a pointer), I try to consider another pattern.

Pattern 2: Return Boxed Values

Let’s move the database connection off the function stack and into the heap. In our own types, we’ll need to explicitly heap allocate data using the Box struct from the standard library. So, refactoring our above code to use the heap would look like this:

fn connect(url: &str) -> Box<Connection> {
    let pool: Pool = postgres::connection_pool::new(url);
    let connection: Connection = pool.connect();
    connection.set_database("startup.io");
    Box::new(connection)
}

There are two changes:

  • The function signature now has Box<..> as the return type instead of &..
  • The function body instantiates a new box wrapping the connection expression Box::new(..) instead of &..

The boxed value is an owned struct (not a reference) so it can be returned from the function without angering the borrow checker. When the box itself goes out of scope, the heap allocated memory will be freed.

Pros:

  • Memory - The only additional memory (beyond the boxed value) is the pointer to the heap, and pointers are only a few bytes
  • Applicability - Almost any code which uses std can leverage this technique

Cons:

  • Indirection - we’ll need to write more code in our type annotations, and we may need to understand how to leverage the Deref trait to work with boxed values
  • Overhead - It’s more complicated to allocate memory on the heap and this may incur a (small) runtime penalty. Profile code with and without boxes to decide if this is an issue.

For types like usize, bool, f32, and other primitive types, it’s a code smell if I find myself boxing these values. Instead I normally return a copy of these types instead.

For struct types or dynamic growable types like Vec, String, and HashMap, it probably makes a lot of sense to box these values.

Pattern 3: Move Owned Values to a Higher Scope

This technique involves reorganizing code to help us leverage references passed into functions. Here’s an example:

fn setup_connection(connection: &Connection) -> &Connection {
   connection.set_database("startup.io");
   connection
}

fn main() {
    let pool = postgres::connection_pool::new(url);
    for _ in 0..10 {
      let connection = setup_connection(&pool.connect());
      // Do something with connection here
    }
}

We achieved this by:

  • moving both the Pool and Connection objects to a scope outside the function
  • changing the function signature to take in a reference to the connection we wanted to modify
  • returning a reference to the same memory as the memory borrowed in the argument

At the end of setup_connection(), no new structs were declared inside of it. This means nothing needs to be deallocated. Because connection was declared inside the for loop, it lives for one iteration of the loop. In the next iteration, a new connection is allocated. Basically, we can only get the lifetimes out of a function that we put into the function.

In this particular case, no matter how many times we call setup_connection(), only one Pool object will be allocated, compared to having many pools allocated in the other patterns.

Pros:

  • Memory - this pattern avoids heap allocating and duplicating data, so it uses the least memory of the three patterns
  • Aroma - this pattern is often the natural result of good code organization that reduces unnecessary work; its like a good code smell

Cons:

  • Complexity - this pattern requires deep understanding of the application and data flows, and usually means rewriting several different areas of the code
  • Applicability - many times this pattern can’t be used
  • Rigidness - using this pattern might make refactoring the code more difficult

This pattern is likely to work when we have an owned value which “emits” other objects. If we can move the owned struct to a higher scope, we can pass referencecs to helper functions. When I’m looking to refactor and/or reduce memory usage, I examine if this pattern might be an option.

More patterns?

Please, if you have experience using more patterns than I’ve enumerated here, please share them! I’d love to incorporate your patterns into this article as a sort of reference of techniques to deal with the borrow checker.