Skip to content

← Closures and Iterators step 7 of 28

Medium Primitives

The double-reference wall: &&x, copied and cloned

Filter two borrowed slices and return owned results.

pub fn evens_then_names(nums: &[i64], names: &[String]) -> (Vec<i64>, Vec<String>)
  • .0 — every even number from nums, in order.
  • .1 — every name whose byte length is at least 4, in order.

For nums = [1, 2, 3, 4] and names = ["ab", "abcd", "xyz"] the answer is ([2, 4], ["abcd"]).

Both parameters are shared slices. You are borrowing. The caller still owns everything and uses it after you return, so you may not move a single element out — the outputs have to be copies and clones. That constraint is the whole exercise.

The starter is the code you would have written

let evens: Vec<i64> = nums.iter().filter(|n| n % 2 == 0).collect();
let long_names: Vec<String> = names.iter().filter(|s| s.len() >= 4).collect();

Nothing about that looks wrong, and it produces three errors:

error[E0369]: cannot calculate the remainder of `&&i64` divided by `{integer}`
error[E0277]: a value of type `Vec<i64>` cannot be built from an iterator
              over elements of type `&i64`
error[E0277]: a value of type `Vec<String>` cannot be built from an iterator
              over elements of type `&String`

&&i64. Two ampersands. Where did the second one come from?

Where the second & comes from

Two facts, and they compose:

  1. slice.iter() yields &T, not T. It is a borrowing iterator; that is its entire job. So nums.iter() has Item = &i64.

  2. Iterator::filter is declared as

    fn filter<P>(self, predicate: P) -> Filter<Self, P>
    where P: FnMut(&Self::Item) -> bool;

    Look at the &. filter hands its predicate a reference to the item, because filtering must not consume what it is inspecting — an item that passes has to survive to be yielded.

Compose them: Item = &i64, so &Self::Item = &&i64. Your closure parameter is a double reference. This is not a quirk; it is the only signature filter could have.

&&i64 still auto-derefs for method callss.len() works fine on a &&String, which is why the string filter’s predicate compiles. It does not auto-deref for operators, because operators are trait impls and the standard library only provides Rem down to one level of reference. Hence E0369 on n % 2.

Four fixes, and when each is right

// 1. Destructure in the pattern. Zero cost, reads well for Copy types.
nums.iter().filter(|&&n| n % 2 == 0)

// 2. Dereference in the body.
nums.iter().filter(|n| **n % 2 == 0)

// 3. Change the item type before you filter — usually the best answer.
nums.iter().copied().filter(|n| n % 2 == 0)

// 4. Take the slice by value where you can.
nums.iter().copied()  ->  or accept an owned Vec and use into_iter()

Option 3 is the one to reach for. copied() turns an Iterator<Item = &T> into an Iterator<Item = T> for T: Copy, and from that point on everything downstream is one reference simpler. Fixing the item type once beats fixing every closure that touches it.

The second E0277 is the same story at the other end of the chain. collect() builds a Vec<String> and you are handing it &Strings. cloned() is copied() for T: Clone — it calls .clone() on each item.

The naive fix is also rejected — and that is the good part

Everyone’s first instinct is .map(|x| *x) or .map(|x| x.clone()). Try it. Clippy’s default-on map_clone catches both:

error: you are using an explicit closure for copying elements
       help: consider calling the dedicated `copied` method

error: you are using an explicit closure for cloning elements
       help: consider calling the dedicated `cloned` method

And on the String side, .map(|s| *s) does not even get that far:

error[E0507]: cannot move out of `*x` which is behind a shared reference

*x on a &i64 is a copy — legal, because i64: Copy. *s on a &String is a move out of a borrow — illegal, always, because moving would leave the caller’s vector holding a hole. Same syntax, opposite outcomes, and the difference is Copy. If you understand why those two lines differ, you understand most of what Copy is for.

copied or cloned?

Both exist, and using the wrong one is not a compile error — cloned() works on Copy types too, since every Copy type is Clone. Prefer copied when the type is Copy: it says at the call site that the operation is a bitwise copy and cannot run arbitrary user code. There is a clippy lint for this (cloned_instead_of_copied), but it is pedantic and off by default, so the gate will not catch it for you here. Do it because it is true, not because you were forced.

Where to put the clone

On the String side, order matters for a reason clippy will tell you about:

names.iter().cloned().filter(|s| s.len() >= 4).collect()
error: unnecessarily eager cloning of iterator items
       [clippy::iter_overeager_cloned]

That version clones every name and then throws most of them away. Filter first, clone the survivors. The lint is default-on, so this one is graded.

Grade is compile + tests + clippy -D warnings.

Loading visualization…