Skip to content

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

Easy Primitives

Copy: the types that don't move

Translate a list of points and report their centroid — using a Point that does not move.

pub fn centroid_path(points: Vec<Point>, dx: i64, dy: i64) -> Vec<Point>

The first question every beginner asks

Why does i32 behave differently from String?

let a = 5;
let b = a;
println!("{a}");        // fine

let s = String::from("hi");
let t = s;
println!("{s}");        // E0382

The honest answer is that one bit of trait information changes whether the source is invalidated. If a type implements Copy, assignment duplicates and both names stay valid. If it does not, assignment moves and the source dies. That is the entire rule. There is no special case for integers, no list of “primitive” types the compiler knows about — i32 is Copy because the standard library says impl Copy for i32, and your own types can say the same thing.

The condition for being allowed to say it is: every field is Copy, and the type has no destructor. Which makes sense. Copy means “duplicating the bytes produces a second, fully independent value”, and a String field breaks that — duplicating its three words gives you two owners of one heap buffer.

Two facts to bank now, because they will save you weeks in the next track:

  • &T is always Copy. Shared references duplicate freely; that is what makes them ergonomic.
  • &mut T is never Copy. Exclusive means exclusive. A &mut that could be duplicated would not be exclusive, so passing one into a function moves it.

The task

The grader builds Point { x, y } values from JSON and reads .x / .y back out of whatever you return, so keep the type name and both field names exactly as the starter has them.

Return a Vec<Point> containing:

  1. every input point, translated by (dx, dy), in order; then
  2. one final point: the centroid of the original (untranslated) points, computed as the sum of the coordinates divided by the number of points using ordinary i64 division. If there are no points, the final point is the origin (0, 0) and it is the only element.

Two fixed helpers, both taking a Point by value:

fn shifted(p: Point, dx: i64, dy: i64) -> Point
fn accumulate(total: Point, p: Point) -> Point

Each point therefore has to reach both. Under the starter’s derive that is E0382. Under the right derive it is free.

The compile-time check

The starter contains this. Do not delete it:

const fn assert_copy<T: Copy>() {}
const _: () = assert_copy::<Point>();

It does nothing at runtime. It exists so that “make Point Copy“ is a requirement the compiler enforces rather than a suggestion you could dodge by sprinkling .clone() around. If Point is not Copy you get:

error[E0277]: the trait bound `Point: Copy` is not satisfied
help: consider annotating `Point` with `#[derive(Copy)]`

Static assertions like this are a real technique, not a teaching prop — const items are type-checked, so any trait bound you can write is a bound you can assert at compile time for zero cost.

::: question #[derive(Copy)] on its own does not compile. Why, and what else can go wrong with these derives? Copy has Clone as a supertrait: pub trait Copy: Clone {}. A type cannot be Copy without also being Clone, so you always write #[derive(Clone, Copy)]. (For a Copy type, the derived Clone compiles down to a plain byte copy — there is no cost to having both.)

The other failure is E0204. Try adding a String field to Point and the derive is rejected:

error[E0204]: the trait `Copy` cannot be implemented for this type;
              field `label` does not implement `Copy`

That is the rule stated as a diagnostic. There is also a third exclusion — a type with a Drop impl can never be Copy — which gets its own article later in this track, because the reasoning behind it is more interesting than it first looks. :::

Don’t reach for clone

Once Point is Copy, writing p.clone() is not a mistake the compiler catches — it compiles and does the right thing. Clippy catches it:

error: using `clone` on type `Point` which implements the `Copy` trait
       help: try dereferencing it: `p`

clone_on_copy is one of the very few ownership lints that is on by default, and it is a genuinely useful gate: it tells you, mechanically, that you reached for a tool you did not need. Take the hint.

One design note: resist making this a generic Point<T>. #[derive(Copy)] on a generic struct silently adds a T: Copy bound, which produces a type that is Copy for some T and not others, and debugging that is a lesson for a much later track.

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…