You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: content/lessons/02_ownership/index.md
+74-45Lines changed: 74 additions & 45 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,9 +1,9 @@
1
1
+++
2
2
title = "Ownership Model"
3
-
date = 2029-01-01
3
+
date = 2025-10-08
4
4
weight = 1
5
5
[extra]
6
-
lesson_date = 2029-01-01
6
+
lesson_date = 2025-10-09
7
7
+++
8
8
9
9
## Why all the fuss?
@@ -12,16 +12,26 @@ Even if you've never seen Rust code before, chances are you still heard the term
12
12
13
13
-**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.
14
14
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
+
15
17
-**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.
16
18
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
+
17
29
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**.
18
30
19
31
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.
20
32
21
33
_**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._
22
34
23
-
## Start with the basics - ownership
24
-
25
35
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:
26
36
27
37
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
34
44
35
45
```rust
36
46
{
37
-
leta:i32=5; // allocation on the stack, 'a' becomes an owner
47
+
// Allocation on the stack, 'a' becomes an owner.
48
+
leta:i32=5;
38
49
39
-
//do some stuff with 'a'
50
+
//Do some stuff with 'a'.
40
51
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.
42
54
```
43
55
44
56
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:
45
57
46
58
```rust
47
59
{
48
-
lets=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
+
lets=String::from("a string");
50
63
51
64
// 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.
54
67
```
55
68
56
69
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++
60
73
Let's expand on the last example. The scoping is not really important for that one, so we don't include it here.
61
74
62
75
```rust
63
-
lets=String::from("a string"); // same thing, 's' is now an owner
76
+
// Same thing, 's' is now an owner.
77
+
lets=String::from("a string");
64
78
65
-
lets2=s; // easy, 's2' becomes another owner... right?
79
+
// Easy, 's2' becomes another owner... right?
80
+
lets2=s;
66
81
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);
68
84
```
69
85
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.
71
87
72
88
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:
73
89
@@ -88,11 +104,11 @@ error[E0382]: borrow of moved value: `s`
88
104
_"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:
89
105
90
106
```rust
91
-
lets=String::from("a string"); // 's' is an owner
107
+
lets=String::from("a string"); // 's' is an owner.
92
108
93
-
lets2=s.clone(); // 's2' now contains its own copy
109
+
lets2=s.clone(); // 's2' now contains its own copy.
94
110
95
-
println!("And the contents are: {}", s); //success!
111
+
println!("And the contents are: {}", s); //Success!
96
112
```
97
113
98
114
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
123
139
124
140
### Exercise
125
141
126
-
How to fix that code?
142
+
How to fix that code? Don't worry about efficiency yet.
127
143
128
144
```rust
129
145
fncount_animals(num:u32, animal:String) {
@@ -139,9 +155,14 @@ fn main() {
139
155
}
140
156
```
141
157
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
+
142
163
## Let's borrow some books
143
164
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:
letmutbook=String::from("Merry lived in a big old house. The end.");
208
229
209
-
letr=&book; //an immutable borrow
230
+
letr=&book; //An immutable borrow.
210
231
211
-
erase_book(&mutbook); //a mutable borrow
232
+
erase_book(&mutbook); //A mutable borrow.
212
233
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.
215
236
216
237
println!("{}", book);
217
238
}
@@ -223,13 +244,13 @@ Fortunately for us (and our poor friend just wanting to read), the compiler step
223
244
error[E0502]: cannot borrow `book` as mutable because it is also borrowed as immutable
224
245
--> src/main.rs:14:14
225
246
|
226
-
12 | let r = &book; // an immutable borrow
247
+
12 | let r = &book; // An immutable borrow.
227
248
| ----- immutable borrow occurs here
228
249
13 |
229
-
14 | erase_book(&mut book); // a mutable borrow
250
+
14 | erase_book(&mut book); // A mutable borrow.
230
251
| ^^^^^^^^^ mutable borrow occurs here
231
252
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
233
254
| - immutable borrow later used here
234
255
```
235
256
@@ -239,7 +260,9 @@ This is where the famous borrow checker comes in. To keep things super safe, Rus
239
260
240
261
- There is any number of immutable references and no mutable ones.
241
262
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.
243
266
244
267
### Dangling references
245
268
@@ -277,7 +300,7 @@ The message above suggests specifing a lifetime for the returned string. In Rust
277
300
278
301
### Exercise
279
302
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?
281
304
282
305
```rust
283
306
fncount_animals(num:u32, animal:String) {
@@ -289,7 +312,7 @@ fn main() {
289
312
290
313
count_animals(1, s.clone());
291
314
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?
293
316
}
294
317
```
295
318
@@ -302,16 +325,17 @@ _**Note:** for the purposes of these examples we assume we are working with ASCI
302
325
To create a string slice from the `String` object `s`, we can simply write:
303
326
304
327
```rust
305
-
letslice=&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
+
letslice=&s[1..3];
306
330
```
307
331
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:
309
333
310
334
```rust
311
-
letslice=&s[2..]; //everything from index 2 till the end
312
-
letslice=&s[..1]; // only the first byte
313
-
letslice=&s[..]; //the whole string as a slice
314
-
letslice=s.as_str(); //also the whole string
335
+
letslice=&s[2..]; //Everything from index 2 till the end.
336
+
letslice=&s[..1]; //From beginning to first byte (so, only the first byte).
337
+
letslice=&s[..]; //The whole string as a slice.
338
+
letslice=s.as_str(); //Also the whole string.
315
339
```
316
340
317
341
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
325
349
Slices can also be taken from arrays:
326
350
327
351
```rust
328
-
letarray: [i32; 4] = [42, 10, 5, 2]; //creates an array of four 32 bit integers
329
-
letslice:&[i32] =&array[1..3]; //results in a slice [10, 5]
352
+
letarray: [i32; 4] = [42, 10, 5, 2]; //Creates an array of four 32 bit integers.
353
+
letslice:&[i32] =&array[1..3]; //Results in a slice [10, 5].
330
354
```
331
355
332
356
### Exercise
333
357
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?
335
359
336
360
```rust
337
361
fncount_animals(num:u32, animal:&String) {
@@ -344,17 +368,22 @@ fn main() {
344
368
count_animals(1, &s);
345
369
count_animals(2, &s);
346
370
count_animals(3, &s);
371
+
count_animals(4, "goat"); // In this version of the code it doesn't compile.
0 commit comments