Mutability and the rule of one
Now we are ready to learn about a central rule in Rust regarding borrows, which is called the rule of one.
The rule of one
You might have noticed that in Rust, variables and borrow are constant by default, which means you can't assign to them:
pub fn main() { let val : i32 = 42; let val_ref = &val; val = 47; *val_ref = 49; }
Any let
binding (the fancy term for a variable in Rust) is immutable by default, so we can't assign to it twice. Any borrow is also immutable by default. To change this, we can use the mut
keyword, making both let
bindings and borrows mutable (i.e. writeable):
pub fn main() { let mut val : i32 = 42; val = 47; let val_ref = &mut val; *val_ref = 49; }
Notice a little difference between the two examples? The one with mut
has the statements in a different order. What happens if we use the old order?
pub fn main() { let mut val : i32 = 42; let val_ref = &mut val; val = 47; *val_ref = 49; }
Now we get an error: error[E0506]: cannot assign to 'val' because it is borrowed
. Here we have stumbled upon another rule that Rust imposes on borrowed values: You must not change a borrowed value through anything else but a mutable borrow! In our example, we try to mutate val
while still holding a borrow to it, and this is not allowed. With just this simple example, it is a bit hard to understand why such a rule should exist in the first place, but there is a subtle C++ example which illustrates why this rule is useful:
#include <iostream>
#include <vector>
int main() {
std::vector<int> values = {1,2,3,4};
int& first_value = values[0];
std::cout << first_value << std::endl;
values.push_back(5);
std::cout << first_value << std::endl;
return 0;
}
Depending on your compiler, this example might be boring (because we got lucky), or it might be very confusing. We take a std::vector
with four elements and get a reference to the first element with value 1
. We then push a value to the end of the vector and examine our reference to the first element again. Compiling with gcc gives the following output:
1
0
Why did the first value change, if we added something to the end of the vector?? Try to find out for yourself what happens here first, before reading on.
Mutability is the root of all evil
So what exactly happened in the previous example? Remember how we implemented our SillyVector
class? In order to keep growing our dynamic array, we sometimes had to allocate a new, larger array and copy values from the old array into the new array. If we do that, the addresses of all values in the vector change! Since a reference is nothing more than a fancy memory address, we end up with a reference that points into already freed memory. Which is undefined behavior, explaining why it is perfectly reasonable that we see a value of 0
pop up. If you have some experience with C++, you might know that it is dangerous to take references to elements inside a vector, however not even the documentation mentions this fact! Well, it does, just not for std::vector::operator[]
, where you might have looked first, instead you have to look at std::vector::push_back
. Here it says: If the new size() is greater than capacity() then all iterators and references (including the past-the-end iterator) are invalidated.
We could have known, but it is a subtle bug. Why did it occur in the first place? And why do we find the relevant information only on the push_back
method, not operator[]
which we use to obtain our reference? The answer to these questions is simple: Mutability.
Mutability refers to things that can change in your program. We saw that in Rust, we have to use the mut
keyword (which is a shorthand for mutable
) to allow changing things, which is the opposite of C++, where everything is mutable and we can use const
to prevent changing things. So here is another key difference between C++ and Rust: C++ is mutable-by-default, Rust is immutable-by-default! Why is mutability such a big deal? Hold on to your seats, because this answer might seem crazy:
Mutability is the source of ALL bugs in a program!
While throwing around absolutes is not something that we should do too often, this statement is so thought-provoking that we can make an exception. Did we really just stumble upon the holy grail of programming? The source of ALL bugs in any software? Time to quit university, you have gained the arcane knowledge to write transcendental code forevermore!
Of course there is a catch. Mutability is what makes our code do stuff. If there is no mutability, there is no input and output, no interaction nor reaction, nothing ever happens in our program. So we can't simply eliminate all mutability because we need it. What we can do however is to limit the usage of mutability! Clearly, not every line of code has to mutate some value. Sometimes we just want to read some data, look at some value but don't change it, and sometimes instead of changing a value, we can create a copy and apply our changes to this copy. This is the reason why Rust is immutable-by-default: Mutability is the exception, not the norm!
Now that we know that mutability can be dangerous, the Rust borrow rule that we cannot modify a value that is borrowed makes a lot more sense! A more precise definition of this rule in Rust goes like this:
There can either be exactly one mutable borrow or any number of immutable borrows to the same value at the same time.
With this rule (that takes some getting used to) Rust eliminates a whole class of bugs that can happen due to mutating a value that is still borrowed somewhere else.