Beginner: Does iterating over a vector of Strings move individual elements?

#1

I’m going through O’Reilly book “Programming Rust”. In Chapter 6 “Expressions” there’s an example on page 131:

let strings: Vec<String> = error_messages();

for s in strings { // each String is moved into s here...
    println!("{}", s);
} // ...and dropped here

The comment each String is moved into s here confuses me.

Is it something the compiler actually performs and generates code for? Or since strings isn’t accessible after the for loop the compiler won’t do anything other than usual lifetime checks on s? Or maybe the comment is related to a concept that’s likely to be explained later when iterators are covered?

#2

The for loop desugars to the following.

let mut iter = IntoIterator::into_iter(strings); // strings is moved here
while let Some(s) = iter.next() { // next() moves a string out of the iter
    ...
}

The code that moves the string out is defined in the implementation of Iterator for std::vec::IntoIter. All it does is a shallow read of the String (just in its (ptr, len, capacity) form), and then adjust the std::vec::IntoIter in some way (incrementing an index? A pointer maybe?) to indicate that the element has been read so that it isn’t returned again and so that it doesn’t get dropped when the std::vec::IntoIter is dropped.

If you don’t want to move the strings, you can do for x in strings.iter(), which borrows it. (there is also a shorthand for &strings; I could explain all of this in great detail, including the trait impls that allow for s in &strings to work, but hopefully the book already does that)

Is it something the compiler actually performs and generates code for?

I’m not sure what you mean. Perhaps you are thinking of C++-style moving? There’s actually nothing special about moving in rust in terms of generated code. To put it simply, in rust, every time an expression is passed into a function, returned from a function, or stored to a variable, it is always just memcpy’ed from one place to another. (note that for a vec, I don’t mean that the heap data is copied, just the 24 bytes that live on the stack) This is in contrast to C++ where these operations are overloadable and generally perform deep copies.

A move is just a memcpy of a type that doesn’t implement Copy, and all it means is that rust forbids you from using it again afterwards. (it is statically determined to be uninitialized data now)

(I am oversimplifying things just a wee bit. There are circumstances in which moving something conditionally may cause a boolean “drop flag” to be written to the stack, so that the code can tell whether the value still needs to be dropped at the end of its scope. But there’s still nothing special about the move itself)

5 Likes
#3

Thanks, that’s clearer now!

What I found confusing was what happens to the memory the vector is using after each element is moved to s. The vector doesn’t seem to be modified, I assume the capacity and size stay the same.

When each String is moved out (via memcpy(dest, src, 24)), does Rust zero-out those 24 bytes at src or just leaves src intact because it’s guaranteed (?) that nobody can access them since the vector itself is now owned by the iterator and it can’t be accessed by anyone else?

My background is C++ and my (wrong) instinct was that elements and the vector get dropped at the same time after the end of the loop. That’s not the case, in the meantime I wrote a simple test to see when exactly elements get dropped, whether it’s after each iteration or when the vector itself gets dropped. I also have an inner scope just to check what happens when I move s to an inner variable:

#[derive(Debug)]
struct Element {
}

impl Drop for Element {
    fn drop(&mut self) {
        println!("Dropping {:?}", self);
    }
}

fn main() {
    let v = vec![Element{}, Element{}, Element{}];

    for e in v {
        println!("\n┌─ loop-scope: begin");
        {
            println!("│┌─ inner-scope: begin");
            
            //let x = e;
            
            println!("{:?}", e);
            
            println!("│└─ inner-scope: end");
        }
        println!("└─ loop-scope: end");
    };
}

(Playground)

Output:

┌─ loop-scope: begin
│┌─ inner-scope: begin
Element
│└─ inner-scope: end
└─ loop-scope: end
Dropping Element

┌─ loop-scope: begin
│┌─ inner-scope: begin
Element
│└─ inner-scope: end
└─ loop-scope: end
Dropping Element

┌─ loop-scope: begin
│┌─ inner-scope: begin
Element
│└─ inner-scope: end
└─ loop-scope: end
Dropping Element
#4

It just leaves the bytes in place.

#5

Thanks!