We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Ownership I: Moves, Copy, Clone, Drop step 6 of 20
Clone: explicit, deep, and never free
Run a batch of names through a two-stage pipeline and return the event log.
pub fn pipeline(names: Vec<String>) -> Vec<String>
Beginners do one of two things with .clone(). Either they clone reflexively
whenever the compiler complains, and ship quadratic code without noticing — or
they refuse to clone on principle, and stall for hours on a problem that a
clone would have solved in ten seconds. Both come from the same gap: not knowing
what a clone costs for a specific type. This problem replaces the taboo with
a price list, and it does it by making every clone visible in the output.
The price list
| Type |
What .clone() does |
Cost |
|---|---|---|
i64, bool, char, &T |
copies the bytes | free — and clippy will tell you off for asking |
String, Vec<T> |
one allocation, one memcpy of the whole buffer | O(len) |
Vec<String> |
one allocation for the Vec, plus one per element | O(total bytes) |
Rc<T> / Arc<T> |
bumps a reference count | O(1); no data is duplicated |
HashMap<K, V> |
allocates and clones every key and every value | O(entries) |
The two rows that surprise people are the last two. Rc::clone is cheap by
design — it is how you share ownership on purpose — while cloning a collection
of owned values is deep, all the way down, every time. A .clone() inside a
loop over a Vec<String> is quietly quadratic, and nothing in the toolchain
will warn you: clippy::redundant_clone is a nursery lint, off by default.
That is the honest situation. Rust makes clones explicit so you can see them, not because the compiler polices them.
The scaffolding
The grader injects a small type. You cannot edit it, and you do not need to import anything to use it:
type Log = Rc<RefCell<Vec<String>>>;
struct Tracked { name: String, log: Log }
impl Tracked {
fn new(name: String, log: Log) -> Tracked { // logs "new <name>"
}
impl Clone for Tracked {
fn clone(&self) -> Tracked { // logs "clone <name>"
}
impl Drop for Tracked {
fn drop(&mut self) { // logs "drop <name>"
}
fn new_log() -> Log;
fn snapshot(log: Log) -> Vec<String>;
fn measure(t: Tracked) -> (Tracked, usize); // logs "measure <name>"
fn mark(t: Tracked, note: String) -> Tracked; // logs "mark <name> <note>"
Drop is a trait with one method, and Rust runs it automatically when a value’s
owner goes out of scope. Wiring it to a log turns “when did this die?” into
something a test can assert — which is how the rest of this track is graded.
Note that Clone here is hand-written, not derived, and it logs. That is
the point of the exercise: none of the expected logs contain a clone line.
If you add a clone to appease the compiler, the test tells you, immediately, in
the diff.
What to build
let log = new_log();
{
let mut alive = Vec::new();
for name in names {
// 1. build a Tracked from the name
// 2. run it through `measure`, keeping the count it returns
// 3. run it through `mark` with that count as the note
// 4. keep the result in `alive`
}
} // <- `alive` dies here, and so does everything in it
snapshot(log)
For ["ada", "bo"] the log is:
new ada, measure ada, mark ada 3, new bo, measure bo, mark bo 2, drop ada, drop bo
Two details worth noticing in that output. Everything is created and processed
one name at a time, but nothing is destroyed until the inner block ends — the
Vec owns the values, so they live exactly as long as it does. And when the
Vec finally dies, its elements drop front to back, in the order they were
pushed. (Plain let locals drop in the opposite order. That contrast gets its
own problem later in the track.)
::: question The starter throws away half of what measure returns and then asks for the value again. Why is .clone() the wrong repair?
Because measure already gives the value back. Its signature is
fn measure(t: Tracked) -> (Tracked, usize)
so the fix is to catch both halves:
let (t, n) = measure(t);
That re-binds the name t to the value that came out of the call — a move in,
a move out, zero allocations, and the same Tracked throughout, so the log
stays clean.
measure(t.clone()) also compiles. It allocates a second String for the name,
a second Rc handle for the log, produces a clone ada line in the output, and
then destroys the copy — all to compute a number the function was already
handing you. This is the “returns what it consumed” pattern from the previous
problem in a slightly better disguise, and spotting it is most of what separates
clone-happy Rust from the other kind.
:::
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.