Skip to content

Instantly share code, notes, and snippets.

@oconnor663
Last active December 20, 2023 18:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save oconnor663/dbf96db59e19bf6190f95af54e11adfb to your computer and use it in GitHub Desktop.
Save oconnor663/dbf96db59e19bf6190f95af54e11adfb to your computer and use it in GitHub Desktop.
Why Iterators in Rust should not be Copy

Here's a valid Rust snippet:

fn main() {
    let mut range = 0..5;
    for num in &mut range {
        if num == 2 {
            break;
        }
    }
    for num in range {
        println!("remaining {num}");
    }
}

It prints:

remaining 3
remaining 4

Notably, if I remove the &mut from the third line, it doesn't compile (error[E0382]: use of moved value: `range` ), because Range is not Copy. However, if we implement our own CustomRange iterator that is Copy, we can provoke some weird behavior. First lets keep the code mostly the same and change only the iterator:

#[derive(Clone, Copy)]
struct CustomRange {
    i: u64,
    n: u64,
}

impl Iterator for CustomRange {
    type Item = u64;

    fn next(&mut self) -> Option<u64> {
        if self.i < self.n {
            self.i += 1;
            Some(self.i - 1)
        } else {
            None
        }
    }
}

fn main() {
    let mut range = CustomRange { i: 0, n: 5 };
    for num in &mut range {
        if num == 2 {
            break;
        }
    }
    for num in range {
        println!("remaining {num}");
    }
}
remaining 3
remaining 4

Nothing out of the ordinary so far. But the problem is that the &mut above is no longer required. If we remove it and make the main function look like this:

fn main() {
    let range = CustomRange { i: 0, n: 5 };
    for num in range {
        if num == 2 {
            break;
        }
    }
    for num in range {
        println!("remaining {num}");
    }
}

Now we see the problem:

remaining 0
remaining 1
remaining 2
remaining 3
remaining 4

The behavior is easy to explain once you're staring at it. The IntoIterator::into_iter call implicit in each for loop takes self by value, which for Copy types means we make a copy of them, so the first for loop does not mutate our copy of iter. The compiler kinda sorta hints at this if you keep let mut range in the code, because it'll tell you that the mut there is doing nothing. But if you just write let range without thinking about it, the code has very surprising behavior with no warnings.

The rule that most Rustaceans agree on is that anything that implements Iterator should not implement Copy, to make it harder for confusing code like this to compile. In most cases where callers really want to copy an iterator, they can call .clone() explicitly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment