Memory Safety
Languages like Rust and Swift claim to be "memory-safe". What does that mean?
Rust
Rust's memory safety relies on two main principles: ownership and borrowing.
Every value in Rust has an owner, meaning that at any given point in the code there is one thing that can have read/write control over the value. Ownership can be borrowed temporarily, but this is tracked by Rust's compiler and the violation of any ownership rules does not compile.
This is a different approach to other programming languages. As we have seen so far, C has no such concept of memory safety. Higher-level managed languages, such as Python, Java or C#, handle memory-safety in the runtime. This compromises speed, as well as the requirement of a runtime.
Ownership
Consider the following code:
fn main() {
let original = "Hello, World!".to_string();
let other = original;
println!("{}", original);
}While this is valid in many other languages, Rust will throw an error here because the value is moved.
error[E0382]: borrow of moved value: `original`
--> src/main.rs:4:20
|
2 | let original = "Hello, World!".to_string();
| -------- move occurs because `original` has type `String`, which does not implement the `Copy` trait
3 | let other = original;
| -------- value moved here
4 | println!("{}", original);
| ^^^^^^^^ value borrowed here after moveIn the second line, as soon as we define other, it becomes invalid to use original again. This protects against double-frees, use-after-frees and any other sorts of vulnerabilities that can arise due to dangling pointers.
Borrowing
Instead of copying data or transferring ownership, Rust allows you to borrow data. This borrowing can be done immutably or mutably. There are restrictions on borrowing which prevent data races, iterator invalidation and bugs caused by concurrency.
Immutable Borrowing
Immutable borrowing allows for read-only borrows. There can be an any number of these borrows at any one time:
This compiles correctly, and acts as you would expect.
Mutable Borrowing
Mutable borrowing is a read/write borrow.
However, to prevent bugs such as data races, we cannot have two mutable borrows:
Similarly, we actually can't take an immutable reference either!
So we can have either
one mutable borrow
any number of immutable borrows
But it is not allowed for us to mix and match - the compiler enforces this.
Lifetimes
A lifetime is what the compiler's borrow checker uses to ensure borrows are valid. A lifetime begins when a variable is created, and ends when it is destroyed. The Rust book has a fantastic description of lifetimes, as well as how you can explicitly control them. The idea is that a variable is tied to the existence of other variables, and dies once they die.
Think about this example:
In something like C, x would go out of scope and die once the } ends the scope, while r lives on as a dangling reference to where x would have been. In Rust, the compiler catches this and refuses to compile.
Meanwhile, if we move the println! into the scope defined by the {}, the program compiles fine as x lives long enough for the borrowed value to be used.
No Null Pointers
Rust does not have null pointers - cases must be handled explicitly. This prevents null pointer dereferences and maybe-null bugs.
Resource Acquisition is Initialization
RAII ensures that whenever an object goes out of scope, its destructor is called and its owned resources are freed. This means you never have to manually free memory and protects you against resource leaks (like memory leaks).
Modern C++ actually supports RAII, which is part of a drive to improve C++ memory safety. Rust requires it, however!
Last updated
Was this helpful?