Why do blocks take ownership

Taking a look at block expressions I find that a block can take ownership of a variable:

fn move_by_block_expression() {
    let s = String::from("hell");

    // Move the value out of `s` in the block expression.
    { s }; // ofc, we can assign this to other variable.
    s; //fails, s was moved
}

fn main() {
    move_by_block_expression();
}

The move is unexpected for me. I tried some conceptualisation that I briefly describe below.

  • Treat the block as an anonymous function taking a parameter?

  • Otherwise I wouldn't expect it to be dropped: should happen when a variable goes out of scope which isn't the case here.

    • To cite the docs:

      When an initialized variable or temporary goes out of scope, its destructor is run, or it is dropped.

      I get though, I'm citing a single passage.

  • Another possibility is that the return of the block has something to do with but (&{ s; }); moves, so that can't be the case.

  • Maybe the block is more like an immediately executable closure, that uses move (a closure like (move || s)())

I did read parts of the articles for scopes and destructors.

They say, for example

Given a function, or closure, there are drop scopes for:

  • Each block, including the function body
  • In the case of a block expression, the scope for the block and the expression are the same scope.

That may answer it but I don't exactly understand it.

Maybe my confusion is simply that drop scopes and the general "scopes" are different? So a variable is drop in a drop scope, even if its own scope is longer-lived (like an outer block) ?

Or I wonder whether there is some other way to conceptualise why blocks take ownership / move ?

Oh, I think my answer might've been just a bit further up (that says control-flow, but blocks are control flow):

Each variable or temporary is associated to a drop scope . When control flow leaves a drop scope all variables associated to that scope are dropped in reverse order of declaration (for variables) or creation (for temporaries).

Why, though?

You could, yet I still don't understand why?

It is: via the { s } expression you're effectively re-scoping (moving) s from the scope of the outer fn move_by_...() into the inner { s } itself. On the exit from the inner scope, s is cleaned up.

Well my whole point is that this isn't very clear; "re-scoping" doesn't exist in the documentation afaik. So, my current hypothesis is that drop-scopes override scopes.

What makes you think that "drop scopes" are any different from the regular ones?

There is no distinction to be made in between the "general" scopes and the rest. Every variable needs a memory location: be it on the stack on otherwise. let s = X (roughly) means "place X on the stack and mark that memory as s for future reference/resolution across the current scope or inner to it".

The { s } afterwards (roughly/pre-optimization) means "grab whatever X was in the memory marked as s; copy ("move") it into the { s }; then forget about that first marker completely". When the { ... } block itself is cleaned up, you're back into the scope of the fn() itself: where the s was moved / forgotten / invalidated through { s }. It's as simple as that.

1 Like

ChatGPT answers:

In this exact code, s is not moved. The value of s is just referenced in the block, but not used in a way that moves ownership. So s is still valid after the block.

Let me prove it to you.

(etc.)

Code I used:

fn main() {
    let s = String::from("hello");

    {
        s
    };

    println!("{}", s);
}

I don't think that's accurate. Wouldn't it be more accurate something like: blocks implement the Drop Trait; and call drop on any variable within at the end of it (i.e are drop-scopes) independently of the variable being initialised in an outer scope? (which is what I tried to say earlier.)

You seem to be misunderstanding what a "move" is. Essentially, every use of a value is either creating a reference to it, or moving it (well, there's also creating a pointer, but that's niche). If you have a variable mentioned everywhere and not take a reference to it, then it's moved, full stop. It doesn't matter if this is a move into another variable, function argument, or, as in your example, an empty statement.

Then, value is dropped by the last scope it was moved into, and block is a scope - so if you use variable inside the block, not taking reference to it, and not move it out of the block, the the block if its final scope.

1 Like

I thought it needed to be re-assigned, and otherwise it didn't move.

But a simple example then would be

fn main() {
    let s = String::from("hello");
    s;
    s;
}

In a way, I thought s; on its own was something like _ = s;.

And it actually doesn't compile, since first s; moves the variable.

_ = s; is a wildcard pattern; thus it's treated differently.

Yes, I meant a simple example of my confusion.

I agree although I think it's the underscore expression in my example. Underscore expressions - The Rust Reference

Indeed.

Now I understand what you meant, thanks all!

1 Like