Ownership and Lifetimes
Ownership is the breakout feature of Rust. It allows Rust to be completely memory-safe and efficient, while avoiding garbage collection. Before getting into the ownership system in detail, we will consider the motivation of this design.
We will assume that you accept that garbage collection (GC) is not always an optimal solution, and that it is desirable to manually manage memory in some contexts. If you do not accept this, might I interest you in a different language?
Regardless of your feelings on GC, it is pretty clearly a massive boon to making code safe. You never have to worry about things going away too soon (although whether you still wanted to be pointing at that thing is a different issue...). This is a pervasive problem that C and C++ programs need to deal with. Consider this simple mistake that all of us who have used a non-GC'd language have made at one point:
#![allow(unused)] fn main() { fn as_str(data: &u32) -> &str { // compute the string let s = format!("{}", data); // OH NO! We returned a reference to something that // exists only in this function! // Dangling pointer! Use after free! Alas! // (this does not compile in Rust) &s } }
This is exactly what Rust's ownership system was built to solve.
Rust knows the scope in which the &s
lives, and as such can prevent it from
escaping. However this is a simple case that even a C compiler could plausibly
catch. Things get more complicated as code gets bigger and pointers get fed through
various functions. Eventually, a C compiler will fall down and won't be able to
perform sufficient escape analysis to prove your code unsound. It will consequently
be forced to accept your program on the assumption that it is correct.
This will never happen to Rust. It's up to the programmer to prove to the compiler that everything is sound.
Of course, Rust's story around ownership is much more complicated than just verifying that references don't escape the scope of their referent. That's because ensuring pointers are always valid is much more complicated than this. For instance in this code,
#![allow(unused)] fn main() { let mut data = vec![1, 2, 3]; // get an internal reference let x = &data[0]; // OH NO! `push` causes the backing storage of `data` to be reallocated. // Dangling pointer! Use after free! Alas! // (this does not compile in Rust) data.push(4); println!("{}", x); }
naive scope analysis would be insufficient to prevent this bug, because data
does in fact live as long as we needed. However it was changed while we had
a reference into it. This is why Rust requires any references to freeze the
referent and its owners.