Part of the “Rustober” series.

Coming from any practical programming language to Rust, we need to learn its system of memory ownership which is different to what we’re used to. Even if some concepts are familiar from languages with manual memory management (like C and C++), or the application of them is familiar like in languages with a garbage collector, Rust will surprise you.

I like how simple and elegant the rules of memory ownership are. There are only three:

  1. Each value has an owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.

The second rule, I think, is the most important and gives Rust its power to enforce correctness. In C, you can allocate memory and it’s just there, you “own” it as long as you have a pointer. You can use the memory throughout its lifetime, but it’s an open question who owns it at any one time. When you don’t need the memory anymore, you free it. Because single ownership can’t be enforced, you can try to use memory somewhere else as if it’s still yours — that is a SEGFAULT. After you’ve freed the memory, you can try to free it again somewhere else — that is, too, a SEGFAULT. Finally, you can choose to not free the memory at all, which is a memory leak but it’s fairly safe because your program doesn’t crash.

Rust also allows borrowing a value to use its memory without changing the owner. That comes with its own set of rules:

  1. At any given time, you can have either one mutable reference or any number of immutable references.
  2. References must always be valid.

The rules above prevent the value from being changed from two places at once, or let it be changes from under you. A classic example of that in C is resizing an array. You have a pointer to it in one place and consider it stable and safe (nothing in C is safe). Elsewhere, you resize the array by calling realloc to give it more memory. You get a new pointer to the array and the “stable and safe” pointer immediately becomes invalid. As I like to say, it is now a pointer to a SEGFAULT. Even worse is resizing it because something doesn’t fit and then losing the new pointer.

Tracking ownership is enabled by lifetimes — everything has a lifetime scope, the longest being 'static, the life of the program. Early in Rust development, lifetime annotations were required in most places and that made the code really ugly (I remember my initial impressions of looking at early Rust). However, since then lifetime elision rules have been adopted and now lifetime annotations are needed much less often. Lifetime elision is also very simple (but took a long time to get right!):

  1. Each elided lifetime in the function or closure parameters becomes a distinct lifetime parameter.
  2. If there is exactly one lifetime used in the parameters (elided or not), that lifetime is assigned to all elided output lifetimes.
  3. If the receiver has type &Self or &mut Self (i.e. it’s a method), then the lifetime of that reference to Self is assigned to all elided output lifetime parameters.

For example, a clear violation of these rules is when you pass two different things into a function and return one of them. The compiler cannot tell automatically which one of them will pass its lifetime to the output, so you must tell it by annotating the parameter and output lifetimes.

What I found interesting is that lifetimes are a generic parameter like generic type parameters which means you can do to them things you do to generics e.g. applying trait bounds (especially it comes into play with trait objects). And more importantly, you cannot assign lifetimes arbitrarily, you can only reveal (=annotate) relationships to the compiler between the lifetimes that already exist.