Skip to content

← Ownership I: Moves, Copy, Clone, Drop step 11 of 20

Hard Primitives

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::drop receives &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::read out of a Drop type is exactly the double-free E0509 exists to prevent, and this track forbids unsafe anyway.
  • ManuallyDrop genuinely does let you disassemble a Drop type — you wrap the value, then ManuallyDrop::take its 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…