Why dereferencing moves... But not always?

Hi,

I cannot deeply understand why in this code only the dereferenced variable nd is moved, while cd is not:

enum CannotDeref {
    Entry = 1,
}

struct CanDeref {
    field: i32,
}

fn main() {
    let nd = &CannotDeref::Entry;
    let cd = &CanDeref{field: 0};
    
    if *nd as i32 == 0 { // <- A move occurs here and generates following error:
                         //    move occurs because `*nd` has type `CannotDeref`, which does not implement the `Copy` trait
    };
    
    if (*cd).field == 0 { // <- This is ok, no move occurs here
    };
}

In general I would like to understand how dereference works under the hood. Maybe I am too linked to the dereference a la C++ style.

i32 implements Copy, and CannotDeref does not. Thus *nd will move the value rather than copy it. i32 values are always copied instead.

The reason this is an error is because the deref operator has to return a value, but you have a borrow on that value, so the value can't be moved without invalidating the reference. If you're dereferencing to an i32, it returns a value by copy, rather than move, so the original reference is not invalidated.

In a bit more detail:

https://doc.rust-lang.org/reference/expressions/operator-expr.html#the-dereference-operator

The * (dereference) operator is also a unary prefix operator. When applied to a pointer it denotes the pointed-to location. If the expression is of type &mut T and *mut T , and is either a local variable, a (nested) field of a local variable or is a mutable place expression, then the resulting memory location can be assigned to. Dereferencing a raw pointer requires unsafe .

*x will create a place expression (think of this as an lvalue from C++). If you use this place expression directly, you will either have to move or copy it. This is why *nd as i32 == 0 fails. However, if you use it indirectly, i.e. by accessing a field, then you don't have to move or copy the entire place expression (only the parts that are used). This is why (*cd).field == 0 succeeds, it only needs to access field (which is Copy)

3 Likes

Uhm... But I am dereferencing the reference cd (I mean doing (*cd).field) that is a CanDeref that doesn't implement Copy neither.

The deref is basically ignored there, because it is already implied by your attempt to access the field. If you tried to move the struct, you'd get the same error:

 error[E0507]: cannot move out of `*cd` which is behind a shared reference
  --> src/main.rs:17:12
   |
17 |     let c= *cd;
   |            ^^^
   |            |
   |            move occurs because `*cd` has type `CanDeref`, which does not implement the `Copy` trait
   |            help: consider borrowing here: `&*cd`

So you're never actually trying to move a CanDeref value.

Basically, the . syntax in rust will deref however far it needs to to get to the value you want, but it doesn't actually return values on the way down, it just sort of "gets you to where you're trying to go."

EDIT: Think of it like this: the * and . operators act like 'structural navigation' operators. They don't typically do anything except help you specify what you're trying to bind to. let c = *cd creates a binding named c to a value of type CanDeref, and to do that it needs to move or copy. The expression (*cd).field is an attempt to bind an i32 value (via a parameter on partial_eq), and it doesn't matter if you passed through a CanDeref on the way, because you never created a binding to that intermediate value. (If Rust implicitly created intermediate bindings through * and ., you'd end up in a sore place once you tried to use non-copy or non-shared data.)

I believe you mean like an lvalue in C++, right?

1 Like

yes

clearly I don't know left from right :man_facepalming:

3 Likes

Or clearly you studied Rust more than C++...

1 Like

One more elaboration, when you pattern match, you are using place expressions. This means that successfully pattern matching doesn't always move out of the input!

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=4cc77504eeecbea6b040aac548fd9f63

let foo = String::new();

// doesn't move! In fact it does absolutely nothing at all, a true no op
let _ = foo;

println!("{}", foo);

match expressions also take place expressions as input, so even they don't have to move out of their input! This allows you to match on unsized values like slices!

let x = [0, 1, 2];
let x = &x as &[i32];

// note *x: [i32], which is !Sized
match *x {
    [] => println!("empty"),
    [_, ..] => println!("one"),
    _ => println!("many"),
}

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.