Skip to content

Commit 335698a

Browse files
committed
Merge branch 'main' into update-task-1-link
2 parents 5ceac34 + 9ea28b6 commit 335698a

File tree

1 file changed

+74
-45
lines changed

1 file changed

+74
-45
lines changed

content/lessons/02_ownership/index.md

Lines changed: 74 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
+++
22
title = "Ownership Model"
3-
date = 2029-01-01
3+
date = 2025-10-08
44
weight = 1
55
[extra]
6-
lesson_date = 2029-01-01
6+
lesson_date = 2025-10-09
77
+++
88

99
## Why all the fuss?
@@ -12,16 +12,26 @@ Even if you've never seen Rust code before, chances are you still heard the term
1212

1313
- **Garbage Collection** - in many high-level programming languages, like Java, Haskell or Python, memory management is done fully by the language, relieving the programmer from this burden. This prevents memory leaks and memory related errors (like _use after free_), but does come at a cost - there is a runtime overhead, both memory and performance wise, caused by the constantly running garbage collection algorithms and the programmer usually has very little control over when the garbage collection takes place. Also, garbage collection does not prevent concurrency-related errors, such as data races, in any way.
1414

15+
There's one language that has a lot of common parts with Rust, but it differs by having a garbage collector: OCaml. In Jane Street, programmers heavily use OCaml as their primary language, and the drawbacks brought by garbage collectors are visible. Their programmers decided to extend the OCaml compiler to be able to write code without any allocations (this approach is quite restrictive) to avoid calling the garbage collector, just so their code runs faster. That's quite a roundabout way!
16+
1517
- **Mind your own memory** - in low-level languages and specific ones like C++, performance comes first so we cannot really afford to run expansive bookkeeping and cleaning algorithms. Most of these languages compile directly to machine code and have no language-specific runtime environment. That means that the only place where memory management can happen is in the produced code. While compilers insert these construction and destruction calls for stack allocated memory, it generally requires a lot of discipline from the programmer to adhere to good practices and patterns to avoid as many memory related issues as possible and one such bug can be quite deadly to the program and a nightmare to find and fix. These languages basically live by the _"your memory, your problem"_ mantra.
1618

19+
For example, in C++ we get some help through variable scopes, but whenever we write a function, we have to decide ourselves how to pass the values. It's easy by mistake to copy a value, pass an invalid pointer or use some dangling reference (i.e., one pointing to an already-freed data).
20+
21+
## To discuss during class
22+
23+
- Why languages with garbage collectors are often considered slower? What different garbage collector mechanisms are there?
24+
- Why coding without allocations can avoid using a garbage collector?
25+
- Besides variable scope, does C++ help in any other way with memory management?
26+
27+
## Start with the basics - ownership
28+
1729
And then we have Rust. Rust is a systems programming language and in many ways it's akin to C++ - it's basically low-level with many high-level additions. But unlike C++, it doesn't exactly fall into either of the categories described above, though it's way closer to the second one. It performs no additional management at runtime, but instead imposes a set of rules on the code, making it easier to reason about and thus check for its safety and correctness at compile time - these rules make up Rust's **ownership model**.
1830

1931
In a way, programming in Rust is like pair-programming with a patient and very experienced partner. Rust's compiler will make sure you follow all the good patterns and practices (by having them ingrained in the language itself) and very often even tell you how to fix the issues it finds.
2032

2133
_**Disclaimer:** when delving deeper into Rust below we will make heavy use of concepts like scopes, moving data, stack and heap, which should have been introduced as part of the C++ course. If you need a refresher of any of these, it's best to do so now, before reading further._
2234

23-
## Start with the basics - ownership
24-
2535
In the paragraph above we mentioned a set of rules that comprise Rust's ownership model. [The book](https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html#ownership-rules) starts off with the following three as its very foundation:
2636

2737
1. Each value in Rust is tied to a specific variable - we call that variable its **owner**.
@@ -34,23 +44,26 @@ The third point might make you think about C++ and its automatic storage duratio
3444

3545
```rust
3646
{
37-
let a: i32 = 5; // allocation on the stack, 'a' becomes an owner
47+
// Allocation on the stack, 'a' becomes an owner.
48+
let a: i32 = 5;
3849

39-
// do some stuff with 'a'
50+
// Do some stuff with 'a'.
4051

41-
} // 'a', the owner, goes out of scope and the value is dropped
52+
}
53+
// 'a', the owner, goes out of scope and the value is dropped.
4254
```
4355

4456
So far, so good. Variables are pushed onto the stack when they enter the scope and destroyed during stack unwinding that happens upon leaving their scope. However, allocating and deallocating simple integers doesn't impress anybody. Let's try something more complex:
4557

4658
```rust
4759
{
48-
let s = String::from("a string"); // 's' is allocated on the stack, while its contents ("a string")
49-
// are allocated on the heap. 's' is the owner of this String object.
60+
// 's' is allocated on the stack, while its contents ("a string")
61+
// are allocated on the heap. 's' is the owner of this String object.
62+
let s = String::from("a string");
5063

5164
// do some stuff with 's'
52-
53-
} // 's', the owner, goes out of scope and the String is dropped, its heap allocated memory freed
65+
}
66+
// 's', the owner, goes out of scope and the String is dropped, its heap allocated memory freed.
5467
```
5568

5669
If you recall the RAII (Resource Acquisition Is Initialization) pattern from C++, the above is basically the same thing. We go two for two now in the similarity department, so... is Rust really any different then? There is a part of these examples that we skipped over - actually doing something with the values.
@@ -60,14 +73,17 @@ If you recall the RAII (Resource Acquisition Is Initialization) pattern from C++
6073
Let's expand on the last example. The scoping is not really important for that one, so we don't include it here.
6174

6275
```rust
63-
let s = String::from("a string"); // same thing, 's' is now an owner
76+
// Same thing, 's' is now an owner.
77+
let s = String::from("a string");
6478

65-
let s2 = s; // easy, 's2' becomes another owner... right?
79+
// Easy, 's2' becomes another owner... right?
80+
let s2 = s;
6681

67-
println!("And the contents are: {}", s); // this doesn't work, can you guess why?
82+
// This doesn't work, can you guess why?
83+
println!("And the contents are: {}", s);
6884
```
6985

70-
At first glance everything looks great. If we write this code (well, an equivalent of it) in basically any other popular language, it will compile no issue - but it does not here and there's a good reason why.
86+
At first glance everything looks great. If we write this code (well, an equivalent of it) in basically any other popular language, it will compile without issues - but it does _not_ compile here and there's a good reason why.
7187

7288
To understand what's happening, we have to consult the rules again, rule 2 in particular. It says that there can only be one owner of any value at a given time. So, `s` and `s2` cannot own the same object. Okay, makes sense, but what is happening in this line then - `let s2 = s;`? Experience probably tells you that `s` just gets copied into `s2`, creating a new String object. That would result in each variable owning its very own instance of the string and each instance having exactly one owner. Sounds like everyone should be happy now, but wait - in that case the last line should work no issue, right? But it doesn't, so can't be a copy. Let's see now what the compiler actually has to say:
7389

@@ -88,11 +104,11 @@ error[E0382]: borrow of moved value: `s`
88104
_"value moved here"_ - gotcha! So `s` is being moved to `s2`, which also means that `s2` now becomes the new owner of the string being moved and `s` cannot be used anymore. In Rust, the default method of passing values around is by move, not by copy. While it may sound a bit odd at first, it actually has some very interesting implications. But before we get to them, let's fix our code, so it compiles now. To do so, we have to explicitly tell Rust to make a copy by invoking the `clone` method:
89105

90106
```rust
91-
let s = String::from("a string"); // 's' is an owner
107+
let s = String::from("a string"); // 's' is an owner.
92108

93-
let s2 = s.clone(); // 's2' now contains its own copy
109+
let s2 = s.clone(); // 's2' now contains its own copy.
94110

95-
println!("And the contents are: {}", s); // success!
111+
println!("And the contents are: {}", s); // Success!
96112
```
97113

98114
The compiler is happy now and so are we. The implicit move takes some getting used to, but the compiler is here to help us. Now, let's put the good, old C++ on the table again and compare the two lines:
@@ -123,7 +139,7 @@ It says that `s` was moved because the `String` type doesn't have the `Copy` tra
123139

124140
### Exercise
125141

126-
How to fix that code?
142+
How to fix that code? Don't worry about efficiency yet.
127143

128144
```rust
129145
fn count_animals(num: u32, animal: String) {
@@ -139,9 +155,14 @@ fn main() {
139155
}
140156
```
141157

158+
## To discuss during class
159+
160+
- How does `std::move` work in C++? When can l-value references (`&`) be used, and when r-value references (`&&`)?
161+
- What exactly are the performance benefits in copying primitives?
162+
142163
## Let's borrow some books
143164

144-
We now know how to move things around and how to clone them if moving is not possible. But what if making a copy is unnecessary - maybe we just want to let someone look at our resource and keep on holding onto it once they're done. Consider the following example:
165+
We now know how to move things around and how to clone them if moving is not possible. But what if making a copy is unnecessary - maybe we just want to let someone look at our resource while still holding that value after they're finished. Kind of like references in C++ (but with some major differences). Consider the following example:
145166

146167
```rust
147168
fn read_book(book: String) {
@@ -183,12 +204,12 @@ fn sign_book(book: &mut String) {
183204
}
184205

185206
fn main() {
186-
// note that the book has to be marked as mutable in the first place
207+
// Note that the book has to be marked as mutable in the first place.
187208
let mut book = String::from("Merry lived in a big old house. The end.");
188209

189-
sign_book(&mut book); // it's always clear when a parameter might get modified
210+
sign_book(&mut book); // It's always clear when a parameter might get modified.
190211

191-
println!("{}", book); // book is now signed
212+
println!("{}", book); // Book is now signed.
192213
}
193214
```
194215

@@ -206,12 +227,12 @@ fn read_book(book: &String) {
206227
fn main() {
207228
let mut book = String::from("Merry lived in a big old house. The end.");
208229

209-
let r = &book; // an immutable borrow
230+
let r = &book; // An immutable borrow.
210231

211-
erase_book(&mut book); // a mutable borrow
232+
erase_book(&mut book); // A mutable borrow.
212233

213-
read_book(r); // would be pretty sad to open a blank book when it was not
214-
// what we borrowed initially
234+
read_book(r); // Would be pretty sad to open a blank book when it was not
235+
// what we borrowed initially.
215236

216237
println!("{}", book);
217238
}
@@ -223,13 +244,13 @@ Fortunately for us (and our poor friend just wanting to read), the compiler step
223244
error[E0502]: cannot borrow `book` as mutable because it is also borrowed as immutable
224245
--> src/main.rs:14:14
225246
|
226-
12 | let r = &book; // an immutable borrow
247+
12 | let r = &book; // An immutable borrow.
227248
| ----- immutable borrow occurs here
228249
13 |
229-
14 | erase_book(&mut book); // a mutable borrow
250+
14 | erase_book(&mut book); // A mutable borrow.
230251
| ^^^^^^^^^ mutable borrow occurs here
231252
15 |
232-
16 | read_book(r); // would be pretty sad to open a blank book when it was not
253+
16 | read_book(r); // Would be pretty sad to open a blank book when it was not
233254
| - immutable borrow later used here
234255
```
235256

@@ -239,7 +260,9 @@ This is where the famous borrow checker comes in. To keep things super safe, Rus
239260

240261
- There is any number of immutable references and no mutable ones.
241262

242-
You may notice a parallel to the _readers - writers_ problem from concurrent programming. In fact, the way Rust's borrow checker is designed lends itself incredibly well to preventing data race related issues.
263+
Those rules are the core of the borrow checker. Be sure that you understand it.
264+
265+
You may notice a parallel to the _readers - writers_ problem from concurrent programming. Because of that, the way Rust's borrow checker is designed lends itself incredibly well to preventing data race related issues.
243266

244267
### Dangling references
245268

@@ -277,7 +300,7 @@ The message above suggests specifing a lifetime for the returned string. In Rust
277300

278301
### Exercise
279302

280-
Our previous solution using `clone()` was pretty inefficient. How should this code look now?
303+
Our previous solution using `clone()` was pretty inefficient. How should you modify this code now?
281304

282305
```rust
283306
fn count_animals(num: u32, animal: String) {
@@ -289,7 +312,7 @@ fn main() {
289312

290313
count_animals(1, s.clone());
291314
count_animals(2, s.clone());
292-
count_animals(3, s); // we could've ommitted the clone() here. Why?
315+
count_animals(3, s); // We previously could've omitted the clone() here. Why?
293316
}
294317
```
295318

@@ -302,16 +325,17 @@ _**Note:** for the purposes of these examples we assume we are working with ASCI
302325
To create a string slice from the `String` object `s`, we can simply write:
303326

304327
```rust
305-
let slice = &s[1..3]; // creates a slice of length 2, starting with the character at index 1
328+
// Creates a slice of length 2, starting with the character at index 1.
329+
let slice = &s[1..3];
306330
```
307331

308-
This makes use of the `&` operator and Rust's range notation to specify the beginning and end of the slice. Thus, we can also write:
332+
This makes use of the `&` operator and Rust's range notation (analogous to Python's notation) to specify the beginning and end of the slice. Thus, we can also write:
309333

310334
```rust
311-
let slice = &s[2..]; // everything from index 2 till the end
312-
let slice = &s[..1]; // only the first byte
313-
let slice = &s[..]; // the whole string as a slice
314-
let slice = s.as_str(); // also the whole string
335+
let slice = &s[2..]; // Everything from index 2 till the end.
336+
let slice = &s[..1]; // From beginning to first byte (so, only the first byte).
337+
let slice = &s[..]; // The whole string as a slice.
338+
let slice = s.as_str(); // Also the whole string.
315339
```
316340

317341
You might have noticed that we always built `String` values using the `from()` method and never actually used the string literals directly. What type is a string literal then? Turns out it's the new string slice we just learned about!
@@ -325,13 +349,13 @@ In fact, it makes a lot sense - string literals, after all, are not allocated on
325349
Slices can also be taken from arrays:
326350

327351
```rust
328-
let array: [i32; 4] = [42, 10, 5, 2]; // creates an array of four 32 bit integers
329-
let slice: &[i32] = &array[1..3]; // results in a slice [10, 5]
352+
let array: [i32; 4] = [42, 10, 5, 2]; // Creates an array of four 32 bit integers.
353+
let slice: &[i32] = &array[1..3]; // Results in a slice [10, 5].
330354
```
331355

332356
### Exercise
333357

334-
Can this code still be improved from the previous version utilizing references? Think about the signature of `count_animals`.
358+
Can this code be further modified utilizing references? Think about the signature of `count_animals`, can we make it also accept string literals?
335359

336360
```rust
337361
fn count_animals(num: u32, animal: &String) {
@@ -344,17 +368,22 @@ fn main() {
344368
count_animals(1, &s);
345369
count_animals(2, &s);
346370
count_animals(3, &s);
371+
count_animals(4, "goat"); // In this version of the code it doesn't compile.
347372
}
348373
```
349374

350-
### Further reading
375+
### Obligatory reading
376+
377+
- [The Book, chapter 4](https://doc.rust-lang.org/stable/book/ch04-00-understanding-ownership.html)
378+
379+
- [Rust by Example, chapter Scoping rules](https://doc.rust-lang.org/stable/rust-by-example/index.html)
380+
381+
### Additional reading
351382

352383
- [Char documentation](https://doc.rust-lang.org/std/primitive.char.html)
353384

354385
- [Working with strings in Rust](https://fasterthanli.me/articles/working-with-strings-in-rust)
355386

356-
- [The Book, chapter 4](https://doc.rust-lang.org/stable/book/ch04-00-understanding-ownership.html)
357-
358387
### Assignment 1 (graded)
359388

360389
[ordering in Van Binh](https://classroom.github.com/a/ih0mIzmM)

0 commit comments

Comments
 (0)