198. into_iter() to Transform — Move Owned Items Instead of cloned()

You have a Vec you’re about to throw away, and you want a transformed one. Reaching for iter().cloned() (or iter().map(|x| x.clone())) duplicates every element on the way out — but you owned them already. into_iter() moves them straight through.

This is the afternoon half of this morning’s mem::replace bite: both are about moving owned data forward instead of copying it. There, it was an enum behind &mut self; here, it’s the elements of a collection.

The trap: iter().cloned() on a collection you’re discarding

You want to uppercase a list of names. The list is a local you won’t touch again:

1
2
3
4
5
6
fn shout(names: Vec<String>) -> Vec<String> {
    names
        .iter()                       // yields &String
        .map(|s| s.to_uppercase())    // to_uppercase already allocates a fresh String...
        .collect()
}

That one’s not even the worst case — to_uppercase builds a new String regardless. The real waste shows up when the transform keeps the value and you clone just to own it:

1
2
3
4
5
6
7
fn tag(names: Vec<String>) -> Vec<(String, usize)> {
    names
        .iter()                                  // &String
        .enumerate()
        .map(|(i, s)| (s.clone(), i))            // clone purely to own it
        .collect()
}

Every s.clone() heap-allocates a duplicate of a string you were about to drop. The original names gets freed on return — you paid to copy bytes that were headed for the incinerator.

The fix: into_iter() consumes the collection and hands you owned items

into_iter() on a Vec<String> yields String by value, not &String. The transform now moves each element — no clone, no second allocation:

1
2
3
4
5
6
7
fn tag(names: Vec<String>) -> Vec<(String, usize)> {
    names
        .into_iter()                 // yields String, owned
        .enumerate()
        .map(|(i, s)| (s, i))        // move s straight in
        .collect()
}
1
2
3
let names = vec!["ada".to_string(), "linus".to_string()];
let tagged = tag(names);
assert_eq!(tagged, vec![("ada".to_string(), 0), ("linus".to_string(), 1)]);

Each string’s heap buffer is threaded through by pointer. Zero element copies.

The rule of thumb

If you still need the collection afterward, iter() (borrow) is correct — you can’t move out of something you’re keeping. But the moment the collection is yours to consume and you don’t need it again, into_iter() skips a copy of every element. A for x in v loop already does this (it’s into_iter under the hood); the win is remembering that .map, .filter, and friends can start from into_iter() too.

1
2
3
4
5
6
// keep the source → borrow
let total: usize = names.iter().map(|s| s.len()).sum();
println!("{}", names.len()); // still usable

// done with the source → move
let owned: Vec<String> = names.into_iter().filter(|s| s.len() > 3).collect();

cloned() earns its keep when you genuinely need both the original and a copy. When you don’t, it’s a tax on data you’re about to free.

← Previous 197. Advance a State Machine with mem::replace — Move the Enum Out, No Clone Next → 199. Static Dispatch — Generics Beat Box<dyn Trait> When You Can Afford the Code