Where is it documented in rust reference that function return values are moved, not dropped?

Hello! I'm learning Rust and trying to understand the precise rules around moves and drops. I've written this example to explore the behavior:

struct MyInt(i8);

impl Drop for MyInt {
    fn drop(&mut self) {
        println!("Drop called for MyInt: {}", self.0);
    }
}

fn main() {
    let mut a = MyInt(10);

    a = smth();

    println!("Print from main 1");

    MyInt(40);

    println!("Print from main 2");
}

fn smth() -> MyInt {
    let b = MyInt(20);
    let c = MyInt(30);
    println!("Print from smth");
    b
}

I got the following output:

$ cargo run
   Compiling loops v0.1.0 (/home/filip/rustprojects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
     Running `target/debug/loops`
Print from smth
Drop called for MyInt: 30
Drop called for MyInt: 10
Print from main 1
Drop called for MyInt: 40
Print from main 2
Drop called for MyInt: 20

Looking at it logically, output is clear to me. However, I can't find where in the rust reference this behavior is explained. Specifically, where it is explained that by returning b from the function, the value is "moved out" (and not dropped)? Reference states that the "move out" is only performed in the following cases:

When a place expression is evaluated in a value expression context, or is bound by value in a pattern, it denotes the value held in that memory location.

If the type of that value implements Copy, then the value will be copied.

In the remaining situations, if that type is Sized, then it may be possible to move the value.

Only the following place expressions may be moved out of:

  • Variables which are not currently borrowed.
  • Temporary values.
  • Fields of a place expression which can be moved out of and don’t implement Drop.
  • The result of dereferencing an expression with type Box<T> and that can also be moved out of.

Since smth() is not a place expression, but value expression, "move out of" should not be performed. I also thought that smth() might be classified as a "temporary value":

When using a value expression in most place expression contexts, a temporary unnamed memory location is created and initialized to that value.

But, smth() within a = smth() can't accommodate in anything listed for "place expression context":

The following contexts are place expression contexts:

  • The left operand of a compound assignment expression.
  • The operand of a unary borrow, raw borrow or dereference operator.
  • The operand of a field expression.
  • The indexed operand of an array indexing expression.
  • The operand of any implicit borrow.
  • The initializer of a let statement.
  • The scrutinee of an if let, match, or while let expression.
  • The base of a functional update struct expression.

Since I'm new to Rust, I'm likely missing something. Any help is greatly appreciated!

smth() is simply an expression, which evaluates to value, and this value is assigned to a.

As for b inside fn smth - I guess you can say that it's a temporary (therefore "a place expression which can be moved out"), which exists until the end of a function and then is "moved out" to evaluate the function call? I'm not sure how this is specified precisely, TBH, and not sure even if it ever was specified precisely.

It's performed in the return expression.

It's classified like this, yes.

You are probably mission return in your function. It's there and it does work.

Don't try to learn language (any language) from its reference manual. These are almost impossible to read unless you already know how language works and want to find precise definitions to clarify some corner cases.

It particular it was trivial for me to find out about “invisible return” (even if I knew nothing about it before today) because I knew how Rust works and it was just a matter of finding the precisely place in the reference that would explain how behavior that I know is born from pieces in the manual.

You tried to found the “no” answer in the reference manual — and these are always tricky, because reference tries to avoid duplications (that way it's easy to keep it consistent) and many behaviors that exist as separate in heads of normal Rust programmer are emergent behaviors — they come into being by combinations of other pieces that are described in the manual.

P.S. The funny thing is that I, myself, have learned C++ from the reference manual… yet not from regular reference manual, but from Annotated C++ Reference Manual, It was different from most reference manuals, because each chapter included explanations about why certain things are defined in the way they are defined. It was closer to Rustonomicon that to most reference manuals… the actual “reference” part was as dull and impenetrable as any other reference manual.

1 Like

It says right here: "Variables which are not currently borrowed." - b falls in this category. The move out rule applies to b inside the function, not to smth() at the call site. The function call smth() produces a value that's already been moved out of the original place.

@khimru @consistent-milk12

Okay, value from b is moved to the "designated output location" (whatever that may be). At this moment, b has lost ownership and the "designated output location" has gained ownership over the value. However, where is it explained that the value is further moved from the "designated output location" to a? I would not say that it creates a temporary in this case, since the reference defines a temporary in this way:

When using a value expression in most place expression contexts, a temporary unnamed memory location is created and initialized to that value. The expression evaluates to that location instead, except if promoted to a static. The drop scope of the temporary is usually the end of the enclosing statement.

And this is not a place expression context...

Unless a itself represents the "designated output location" - which I wouldn't say is the case (if it is, I wouldn't call this as "designated output location"). In this way, the value would be moved into a before the function even completes...

1 Like

The core issue here, I think, is that Rust intentionally leaves certain operational details unspecified. The reference describes what happens (the value is moved into a) but not the precise mechanism (where the value exists between return and assignment). This ambiguity allows the compiler freedom to optimize using various strategies (probably very evolving ones too) depending on the situation. The implementation is clear though, the value ends up directly in 'a' with no observable intermediate location. You may have a point that the specification just doesn't formalize that detail. So you are better off waiting for someone actually more knowledgeable about internals :'D

1 Like

In the description of the assignment expression, obviously. It “moves a value into a specified place”, which is a, here.

That's how C++ defines things and thus what is happening in the machine code. There are discussions about bringing that idea to Rust, but currently that's now how language is described.

Again: reference is something you read after you know and understand the language to find out about minutia nuances.

It's really very poor replacement for a tutorial.

No, the core issue is an attempt to learn language from reference. You look on something, see 3 pieces there and couldn't understand, from their description, how these 3 pieces can work, together. When someone who actually know language well looks on it it sees 33 pieces there and can, then, find these 33 pieces in the manual and explain how they work together.

It explains these, too. Just in different parts of the manual. And you need to find them. And for that you need to know how language works.

5 Likes

I kind of agree with you, but I don't think it's needed for most users to understand how things are actually working internally in such details. All languages do this, it would be too overwhelming to pin down how a specific operation in say, python, works, people just have an high level abstract understanding of what's happening to make the algorithms make sense.

Yes, that's true. You only need one guy with deep understanding of Rust on your team (or maybe even few teams).

But you need that knowledge to read reference manual!

And herein lies the rub: if you try to learn language from its reference manual you are immediately in a position where you need to learn bazillion details that most language practitioners don't even know.

In particular I'm not 100% sure I know Rust well enough to read its manual — after using it for a few years.

2 Likes

which is basically good old "if everyting else fails, read the manual" (not sooner!) :slight_smile:

I'm not learning the language from the reference. I'm reading The Book. However, the book doesn't explain this "moving" concept that much in details, so I reached for the reference. :smiley:

If I understood everything correctly, you want to say that each function call has designated output location and before function "ends", function return expression moves its argument to that location. So, we can consider the call expression smth() (which is a value expression) as we have some variable output_location_of_smth_function_call (which is a place expression). Thus, the:

a = smth()

can be considered as:

a = output_location_of_smth_function_call

and since output_location_of_smth_function_call is some kind of place expression in value expression context, move is happening.

I think it would be much better if they add it to the list, like this:

Only the following place expressions may be moved out of:

  • Variables which are not currently borrowed.
  • Output location of function calls.
  • Temporary values.
  • Fields of a place expression which can be moved out of and don’t implement Drop.
  • The result of dereferencing an expression with type Box<T> and that can also be moved out of.
2 Likes

Well that was the case here :sob:

If it makes things easier for you to visualize, you can think of it like that, but the actual implementation details of a = smth() is:

  1. smth() evaluates, producing a temporary value
  2. That temporary value is moved into a
1 Like

Maybe. Try to raise an issue, maybe?

I have no idea why it's not there, honestly. Without it “designated output location”, mentioned in description of return becomes a mystery: it's only mentioned there — and nowhere else, in the whole manual.

“Move” is the simplest concept to imagine, it's explained here: Move constructors are meaningless in Rust because we don't enable types to "care" about their location in memory. Every type must be ready for it to be blindly memcopied to somewhere else in memory.

The interesting question about move is not what it is, but when it's allowed and when it's not allowed (some objects couldn't be copied, just moved). Because in some cases when you “move out” object from some larger structure said structure would be left in the incorrect state — and this should be prevented or program would misbehave.

I think no one was writing what exactly happens when functions return because there are no ambiguity: the whole stack frame of the function is “nuked from the orbit”, thrown away, there's nothing left, the question about whether move out is valid or not is not even worth asking… but yeah, formal description could have been more clear.

3 Likes

The list you are quoting is not a list of when moves can happen. It is a list of, out of all place expressions, which ones can be moved out of rather than only borrowed. A function call is not a place expression at all; it is a value expression.

It seems to me (I have not thoroughly re-read the reference) that what is missing is not anything about place expressions but a discussion of the fact that the normal case is moves — any time an expression is evaluated in a value expression context, the result is a move (or copy) of the value.

I know the list only talks about a subset of all place expressions that can be moved out rather than copied, cloned, or borrowed (I'm not sure why you mentioned only "borrowing" :D).

But I'm not talking about the function call itself (since that's a value expression). I'm talking about the output location of the function call - though it's questionable whether this can even be called a place expression.

It is not a place expression; it's not an expression at all — expressions are parts of the syntax of the language. There is no piece of source text that parses as a expression of this kind.

Agree. That's why I wrote "though it's questionable whether this can even be called a place expression" - since it's not :smiley:

Btw, did you forgot to write copy and cloning together with "borrowed" in:

It is a list of, out of all place expressions , which ones can be moved out of rather than only borrowed

Yes and no. I forgot to mention copying, which is another primitive operation. But "cloning" is calling Clone::clone(), which works through an & borrow and is not primitive.

1 Like

I don't know. Argument of the return expression is moved out/copied to "function call designated output location", but what is happening further is mysterious according to the reference.