The expression without effect moves the variable

I have this code

struct NoCopy;
let b_no_copy: NoCopy = NoCopy {};
let bb: NoCopy = b_no_copy;
bb; // value moved here
let b_ref: &NoCopy = &bb; // Error: borrow of moved value: `bb`

Why is the variable being moved? The most important thing is WHY and WHERE.
It turns out that bb; is not an expression without effect, where can I read about it?
I saw a similar topic about this, but this issue was never properly explained there.

When the type has a destructor, the compiler clarifies that a little more, as in:

struct NoCopy;
impl Drop for NoCopy {
    fn drop(&mut self) {}
}

fn main() {
    let bb: NoCopy = NoCopy {};
    bb;
}
warning: path statement drops value
 --> src/main.rs:8:5
  |
8 |     bb;
  |     ^^^ help: use `drop` to clarify the intent: `drop(bb);`
  |
  = note: `#[warn(path_statements)]` on by default

(note the help suggesting you should write drop(bb) here.)

Without a destructor, the statement indeed has no “effect” in that it doesn’t do anything at run-time and can safely be removed. But it does have the effect that the value is considered moved-out-of after that statement.

By the way, doing let _ = …; instead behaves differently in that it doesn’t move out of a place you provide on the right-hand-side. This kind of stuff is very subtle though, indeed.

As a general rule

EXPRESSION;

statements are, I believe, always be equivalent to

drop(EXPRESSION);
5 Likes

It should be documented in the reference, let’s see… under statements -> expression statements, it starts with

An expression statement is one that evaluates an expression and ignores its result. As a rule, an expression statement's purpose is to trigger the effects of evaluating its expression.

So the purpose of expression statements is the effect of evaluating; so using it just to drop the result of evaluation would go against this purpose (which explains the warning already even for cases where it isn’t fully “without effect” if the dropping is considered an effect). Regarding dropping the return value, this first sentence does however only say it “ignores” its result; and let _ = …; certainly also “ignores” its result, but it’s a different kind of thing.

So I don’t like this documentation, I must admit. I think it would benefit from elaborating, at least slightly, on the “ignores the its result” part, and the effect that this can have in combination with the kind of “evaluation” that’s done to the expression. For more context on what I mean by that, let’s explore this further:


We'll need to look at a relevant different section in the reference, where the information we are after, that statements like bb; move and drop the variable contents, is provided indirectly:

First, under Place Expressions and Value Expressions we get a definition:

The following contexts are place expression contexts:

From this definitionm by process of elimination, we can conclude that “expression statements” (unlike “let statements”) are never “place expression contexts”, so they’re always “value expression contexts”, and the following then applies:

Moved and copied types

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:

After moving out of a place expression that evaluates to a local variable, the location is deinitialized and cannot be read from again until it is reinitialized. In all other cases, trying to use a place expression in a value expression context is an error.

(For the full picture, also note that path expressions like bb are place expressions; I’ll skip looking into the relevant section for that information.)

So we can piece together what happens here: the statement bb; evaluates the value of the variable bb and in this process of obtaining that value, it moves the value out of bb and bb gets deinitialized.

3 Likes

That's a fantastic answer. Thank you!
Okay, I can accept that there is movement occurring here.
However, I can't accept that I don't get any default warning and normal explanation from the compiler.
Judging by your message, this warning refers to "path-statements", however, I do not receive it, despite the fact that rustc -W help says that it is enabled by default.

How did you get such a warning?

warning: path statement drops value
 --> src/main.rs:8:5
  |
8 |     bb;
  |     ^^^ help: use `drop` to clarify the intent: `drop(bb);`
  |
  = note: `#[warn(path_statements)]` on by default

Instead, I have a "path statement with no effect"

How many of these hidden things are there in the language?
This is quite strange for a language that is trying to be a system language.

BTW, if you want a statement without effect, that'd be let _ = bb;.

1 Like

Indeed this has so little effect that that can become somewhat confusing, too.

struct NoCopy;
let bb = NoCopy;
drop(bb);
// &bb; // would fail, cannot access `bb` anymore, but:
let _ = bb; // this is *fine*

Rust Playground

5 Likes

Why would compiler need to provide it? “default warnings” and “normal explanation” are for things that people may misunderstand and use for wrong purpose.

How can you use that code to do something wrongly? You may either understand it properly and misunderstand it and either get a compile-time error (when you would attempt to use moved-out value) or program would work like you expect. Do we even need any warning in the latter case where everything works like it should just for the wrong reason?

Except Rust haven't tried to be a system language initially. It just wanted to be predictable, logical, language instead.

Then people have found out that you may do system development with such a language.

But what exactly is hidden here?

The big question is: why do you expect that expression to an expression without effect?

What if you would have something like this.

   my_super_duper_func(bb);

This wouldn't be “something without effect”, right?

And if we would use drop:

   drop(bb);

This is still not an expression without effect, right?

Okay, drop is defined like this:

pub fn drop<T>(_x: T) {
}

And if we would just manually inline it's body we end up with:

   bb;

Why should this step, suddenly, change the semantic of everything?

The “proper explanation” can only be found if one may find something to explain. Why bb; is not an expression without effect is obvious to me (the rules say that if you take value of variable and use it without & or &mut somewhere then you are moving it somewhere and the fact that we are moving it into fourth dimension like in a Stranger in a Strange Land doesn't change anything), but not for you… can you explained which rules you had in your head when you arrived at the conclusion that bb; shouldn't do anything?

I was confused, at first, by the fact that let _ = bb; doesn't do anything and that required some explanation, but after some thinking I realised that having _ as a non-binding expression is nice and if we have that then let _ = bb; shouldn't do anything, but why bb; should behave like that? That would have been strange and illogical to me.

1 Like

The behavior of expression statements consuming the value can definitely be surprising to people with intuitions preformed from other languages without move semantics, where expression statements have no side effects beyond those contained in the expression itself. The Rust behavior makes sense when one stops to think about it, but it can be easy to fall into the trap of using mental shortcuts formed outside of Rust without thinking too hard about them.

5 Likes

That's certainly true, but I'm just not sure how much weight we have to assign to these mis-intuitions.

The epiphany for me was the moment where I realized that I already have a different intuition which may guide me about Rust design, and the one that I had much longer than any intuition developed in C++ or Java: real world, real objects, everything you see around you.

Rust treats values like real world objects are treated and not like values and variables are treated in other languages.

Every object in the existence which we may interact with have an owner, exactly like in Rust, someone may borrow it, exactly like in Rust or you may move it somewhere, exactly like in Rust. Some objects may be owned by a few entities, of course, exactly like in Rust — and in extreme cases this may lead to strange situation where legal entity exists even it doesn't have even a single real human who may claim s/he is owning said entity.

Yes, making that mental effort to forget about mental shortcuts formed outside of Rust may be not exactly trivial, but if you couldn't do that they you would be forever bumping into strange “warts” in Rust design which are only perceived as “warts” because of these mental shortcuts.

If you give your personal belongings to a charity which wouldn't do anything to them then what would happen to them? They would be disposed after charity would disappear.

This doesn't contradict the intuition in real world, but the exact same operation done on values in a programming language makes you surprised… why?

1 Like

It's an interesting question to ask oneself, I appreciate the thought. I can think of two reasons why, for me personally:

  1. I sometimes think of the computer memory available to my running program as a single object with a single owner: my program or "me" while I'm coding it. The programming objects in that memory don't have a real existence of their own, since I can overwrite them with something different at any time. Of course that's a long outdated viewpoint that probably came from my C programming.

  2. More recently I'm used to thinking of program objects as independent, sort of like we imagine people to be, with no owner at all. This likely comes from my Java programming, where objects exist until they are no longer remembered, also sort of like people. They can be mutated, but so can we.

In both cases, programming objects don't have owners. Why should they? :wink:
:face_with_spiral_eyes:

I think you meant “K&R C” here, not ANSI/ISO C. Because in C computer memory was never a single object with a single owner. Otherwise such code would always produce 1 1 (or nothing) as output:

    if (p == q) {
        *p = 2;
        *q = 1;
        printf("%d %d\n", *p, *q);
    }

In standard C that's not guaranteed, of course.

In fact Rust's memory model, ownership and borrowing and all these things may be traced back to that failed attempt to bring disparate objects into C so many decades ago.

Because if there are some entity without owner confusion and, eventually, war ensues, both in real life and in Rust.

1 Like

For bb; specifically, it feels a bit like simply speaking the name of a demon dispels it. I assume bb;-style constructions mostly occur by accident, and only after doing it for the first time do people stop to consider why it works that way. There are plenty of ways to invoke the name of something in Rust to do far more consequential things, so it can be surprising that doing something so trivial moves the value.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.