We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Ownership I: Moves, Copy, Clone, Drop step 11 of 20
E0509: a Drop type is an atom
Get the payload out of a type that has a destructor — without cloning it, and without upsetting the destructor.
pub fn harvest(sessions: Vec<(i64, String)>) -> Vec<String>
This is the hardest problem in the beginner half of the track, and it is the one with no counterpart in any language you already know. Read the whole page before you start.
The wall
The previous problem’s Record had no destructor, and you took it apart field
by field. Add a Drop impl and the same code stops compiling:
error[E0509]: cannot move out of type `RawSession`, which implements the `Drop` trait
|
| let payload = s.payload;
| ^^^^^^^^^ cannot move out of here
The reason is a single sentence, and once you have it the error becomes obvious rather than arbitrary:
Drop::dropreceives&mut self, so the destructor is entitled to a whole, valid struct — every field still there.
A partial move would leave a hole. When the value dies, Rust must call
drop(&mut self), and that method may read any field it likes. If payload had
walked out, self.payload would be uninitialised memory and the destructor
would read it. Rust’s answer is not to track which fields are still there at
runtime; it is to refuse the move at compile time. A type with a destructor is
an atom. You take all of it or none of it.
Note the asymmetry with the previous problem. Partial moves are fine precisely
because nothing else needs the struct to be whole afterwards — no Drop impl
means no code is entitled to the missing fields.
The two types
The grader injects both. You choose which one to use.
struct RawSession {
id: i64,
payload: String, // <- no way to empty this
log: Log,
}
impl Drop for RawSession {
// logs "close {id} payload=present"
}
struct Session {
id: i64,
payload: Option<String>, // <- a slot that has a legal empty state
log: Log,
}
impl Drop for Session {
// logs "close {id} payload=present" or "close {id} payload=absent"
}
Constructors: RawSession::new(id, payload, log) and Session::new(id, payload, log),
both logging "open {id}" and both wrapping the payload appropriately. Helpers:
new_log(), snapshot(log) -> Vec<String>, note(log, line).
What to build
1. Build a session for every (id, payload) pair, keeping them all alive.
2. Then consume them one at a time. For each one:
- get the payload out
- note "got {id} {payload}"
- let the session die before moving to the next
3. Return the log.
For [(1, "a"), (2, "b")]:
open 1
open 2
got 1 a
close 1 payload=absent
got 2 b
close 2 payload=absent
The payload=absent lines are the assertion that matters. They prove two
things at once: the destructor really did run for every session, and it found
the payload slot already emptied — so the payload left without the struct ever
becoming invalid.
::: question Option<String> is the same size as String. How can adding a variant be free?
Because String‘s first word is a pointer, and a String‘s pointer is
never null — even an empty String holds a dangling-but-aligned non-null
address. That leaves the all-zeros pattern unused, and the compiler spends it
on None. Same layout, no tag, no cost. You measured exactly this for
Option<Box<u8>> earlier in the track.
Which is what makes Option::take the professional answer rather than a
workaround. It is not “wrap it in an Option and pay for the indirection” — it is
“tell the compiler this slot has a legal empty state”, and the compiler charges
you nothing for the information.
:::
The idiom
let payload = s.payload.take(); // -> Option<String>
Option::take writes None into the slot and hands you what was there. From
the compiler’s point of view nothing moved out of s at all — a value was
swapped within it — so the destructor still gets a complete struct, one of
whose fields now happens to be None.
take needs &mut access to the field, so the binding has to be mut:
for mut s in built.
The longhand is std::mem::replace(&mut s.payload, None), and if you write it
clippy will stop you:
error: replacing an `Option` with `None`
help: consider `std::mem::take(&mut s.payload)` or `s.payload.take()`
mem_replace_option_with_none is on by default. It is one of the nicest
lint-as-teacher moments in the language — the compiler will not tell you the
idiom exists, but the linter will.
What not to do
- Do not clone the payload. It compiles, and it is a strictly worse program: you allocate a copy, and the original is still destroyed by the destructor a moment later.
-
Do not reach for
unsafe.ptr::readout of aDroptype is exactly the double-free E0509 exists to prevent, and this track forbidsunsafeanyway. -
ManuallyDropgenuinely does let you disassemble aDroptype — you wrap the value, thenManuallyDrop::takeits fields, accepting that you are now personally responsible for every destructor that no longer runs. It is the right tool inside a data structure’s internals and the wrong tool here. The last problem of this track comes back to it.
Remember the grade is compile + tests + clippy -D warnings.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.