Part 1 Β· The runtime machine
Reading a stack frame
All of Part 1 applied to one real function, line by line β then a mixed set that refuses to tell you which idea it is testing.
What this lesson is trying to break (2)
- I understand each rule separately, so I understand them together.
- Practising one topic at a time is the efficient way to study.
Part 1 gave you five separate rules. This lesson checks whether they work together, which is a different thing β and then deliberately mixes them up.
The function
Read it once, normally, the way you would in a pull request.
use std::collections::HashMap;
#[derive(Debug)]
struct Config {
name: String,
retries: u32,
}
fn load(raw: HashMap<String, String>) -> Option<Config> {
let name = raw.get("name")?.clone();
let retries = raw
.get("retries")
.and_then(|s| s.parse().ok())
.unwrap_or(3);
Some(Config { name, retries })
}
Now answer these before opening any of the steps below.
- Does
loadownraw, or borrow it? - Why is the
.clone()on line 1 of the body necessary, given the answer to question 1? - At what point is the
HashMapβs memory released?
- Owns it. The parameter is
HashMap<String, String>, not&HashMap<...>. The caller moved it in and cannot use it afterwards. - Because
getreturnsOption<&String>β a borrow into the map. Owning the container does not let you move a value out of it through a shared reference; that would leave a hole in the map. So to get an ownedStringyou must clone. (raw.remove("name")would move it out instead β see the walkthrough.) - At the closing brace of
load, when the parameter place goes out of scope. Every key and valueStringis dropped, then the mapβs own allocation is freed.
Question 2 is the one that separates βI read the ownership chapterβ from βI have
the modelβ. Owning a collection and being able to move an element out of it are
genuinely different permissions, and the .clone() is not laziness β it is
forced by the choice of get over remove.
Line by line
Open one step at a time. Try to answer each before you expand it.
Step 1 β `fn load(raw: HashMap<String, String>) -> Option<Config>`
raw is a place owned by this frame. The caller wrote load(m) and m is
now dead at the call site.
On the stack, raw is three words: pointer, length, capacity β the same shape as
a Vec. Every key and value String is itself three words living in the mapβs
heap allocation, each with its own separate buffer.
Cost of the call: three words copied. Not the map contents.
Step 2 β the name binding, and why it must clone
Four things happen on this line:
raw.get("name")takes&rawβ a shared borrow β and returnsOption<&String>.?unwraps it. OnNoneit returnsNonefromloadimmediately. Note what this means for drop: the early return still dropsrawat the exit point..clone()on the&Stringproduces an ownedStringβ a new heap allocation, bytes copied.- The shared borrow of
rawends here, because nothing keeps it alive past this statement.
The alternative worth knowing: raw.remove("name")? returns
Option<String> β owned, no clone, no allocation. It requires raw to be mut
and it mutates the map. Which is right depends on whether the map is used again.
Here it is, on the next line, so get + clone is defensible.
Being able to say βyou could use remove here and skip the allocation, but then
the map is modifiedβ is exactly the level of comment that is useful in a Rust
review.
Step 3 β the `retries` chain
raw.get("retries") // Option<&String>
.and_then(|s| s.parse().ok()) // Option<u32>
.unwrap_or(3) // u32The closure takes s: &String. s.parse() works because of deref coercion:
&String becomes &str, and parse is a method on str. You have not been
taught that yet β it is 3.4 β but note that it
happened silently, because that silence is exactly what makes Rust hard to read
at first.
parse is generic over its return type: fn parse<F: FromStr>(&self) -> Result<F, F::Err>.
Nothing on this line says u32. The compiler infers it backwards from
unwrap_or(3) combined with the retries: u32 field in the struct literal two
lines later.
No allocation happens in this chain. Every step is a borrow or a Copy type.
Step 4 β `Some(Config { name, retries })` and the end of the frame
name is moved into the structβs field place. The String is not copied;
its three words are written into Config.name and the local name is dead.
retries is u32, which is Copy, so the local stays valid β irrelevant here
since nothing uses it again, but the mechanism differs.
The Config is moved into Some(...), then moved out as the return value.
Written out like that it sounds expensive; it is not. These are stack writes the
optimiser almost always collapses into constructing the value once, in place.
Then the frame ends and raw is dropped: every String key and value freed,
then the mapβs allocation. The cloned name is not affected β it was a
separate allocation from the start, and it now lives in the returned Config,
owned by the caller.
Rewrite load to take &HashMap<String, String> instead of owning it. What
changes, and what does the caller gain and lose?
Then answer the harder version: is there a signature that avoids the clone and leaves the callerβs map intact? Say why or why not.
Interleaved practice
Now the part that feels worse and works better.
The questions below are not in lesson order and are not labelled by topic.
Before answering each one you have to work out which idea it is about β and that
sorting step is the thing that actually transfers to reading unfamiliar code.
Blocked practice (all the Drop questions together, then all the move questions)
produces better scores today and worse understanding later. This is one of the
most robust findings in the study of practice, and one of the least used.
let a = vec![1, 2, 3];
let b = a;
let c = b.len();
println!("{c}");Compiles?
Yes. a moves to b, but nothing uses a afterwards. Moving is only an
error if you use the source later. The move itself is always fine.
A common wrong instinct here is that moving is inherently suspicious. It is not β it is the default, and most Rust code moves constantly without anyone noticing.
struct D(&'static str);
impl Drop for D { fn drop(&mut self) { println!("{}", self.0); } }
fn main() {
let _a = D("a");
let _ = D("b");
let _c = D("c");
}b
c
aD("b") is bound to _, which is a wildcard that binds nothing β so it is a
temporary and dies at that semicolon, first. Then at the end of main, the
real bindings drop in reverse order: _c, then _a.
This is 1.4 twice over, and you had to notice
both rules were in play. If you predicted c b a you applied reverse-order
correctly but missed the underscore.
fn main() {
println!("{}", size_of::<Option<Box<[u8]>>>());
}16.
Box<[u8]> is a fat pointer: data pointer plus length, 16 bytes. Wrapping it in
Option adds nothing, because the data pointer is non-null and gives the
compiler a niche to encode None in.
Two ideas from two different lessons had to combine: fat pointers from 1.2, and niche optimisation from the pretest in 0.2.
#[derive(Clone, Copy)]
struct Handle { id: u64, name: String }Does this compile?
No. Two errors, and the second is the interesting one.
Copy cannot be derived because String is not Copy. And Clone can be
derived β that part is fine β but Copy requires Clone, and requires every
field to be Copy.
The rule from 1.3: Copy claims that duplicating the
bytes yields a valid second value. String owns a heap buffer, so it does not.
The fix is to drop Copy and keep Clone, or to change the field to something
like &'static str or a small inline type.
Part 1 checkpoint
Say each of these out loud, without notes. If any of them stalls, the lesson to reread is named.
- What does a
letbinding create? (1.1) - What exactly is copied in
let b = awhena: String? (1.1) - What are the two words in a
&str? In a&dyn Trait? (1.2) - What claim does
impl Copymake? (1.3) - How does
drop(x)work when its body is empty? (1.3) - Which drops first β the first local or the last? The first field or the last? (1.4)
- Where does a temporary die, and what is the exception? (1.4)
If all seven are fluent, you have the runtime machine. Part 2 will now argue with you about which of these programs you are allowed to write β which is a completely separate machine, and the hardest part of the course.