2

I think an object that was moved from one binding to another simply means the object bits themselves stay put; just that program source refers to it with a different binding (identifier).

use std::fmt;

struct Person {
  name: String,
  age: u8,
}

impl Clone for Person {
  fn clone(&self) -> Self {
    Person {
      name: self.name.clone(),
      age: self.age,
    }
  }
}

impl fmt::Pointer for Person {
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    let ptr = self as *const Self;
    fmt::Pointer::fmt(&ptr, f)
  }
}

fn main() {
  let p = Person {
    name: "Krishna".to_string(),
    age: 8,
  };
  println!("{:p}", p);

  let a = p.clone();
  println!("{:p}", a);

  let q = p; // moved
  println!("{:p}", q);
}

This prints

0x7ffee28b4178  // P
0x7ffee28b41f8  // A (P's clone)
0x7ffee28b4260  // Q (moved from P)

Why are the addresses of p and q different? It was compiled with rustc test.rs.

legends2k
  • 31,634
  • 25
  • 118
  • 222
  • 1
    If you want a fixed address, that's what [`Pin`](https://doc.rust-lang.org/std/pin/struct.Pin.html) is for – hellow Feb 07 '20 at 07:32
  • just by using their address you probably disallow number of optimisation. – Stargateur Feb 07 '20 at 08:32
  • @Stargateur Sure, this was just a toy program to understand language concepts better. Thanks! – legends2k Feb 07 '20 at 08:56
  • Thanks for letting us know about `Pin`, but wanting to keep memory location unchanged is a different thing. What I wanted to know was Rust's move under covers. – legends2k Feb 07 '20 at 09:01

1 Answers1

4

Why are the addresses of p and q different?

Rust objects aren't heap-allocated so, unlike in Python or Java, there is no distinction between a binding and the actual object behind it. Variables such as p name locations where the object is actually stored, so it's not surprising that moving the object, well, moves it. Unlike C++, Rust won't run the destructor on the old (moved) object, so a bitwise move is both correct and efficient.

Note that there is no guarantee that an actual move will occur. In an optimized build the compiler might well realize that q can reuse the space occupied by p and optimize the move away. As pointed out in the comments, requesting and using the address might have had the effect of disabling such optimizations.

user4815162342
  • 141,790
  • 18
  • 296
  • 355
  • I'm a C++ programmer, I think I can understand it from an automatic variable (usually stack-allocated) perspective. Thanks! – legends2k Feb 07 '20 at 07:23
  • I understand that there can't be a trivial pointer swap that'll do the move, so what is a move here? How will the compiler implement it for a custom type like `Person`? – legends2k Feb 07 '20 at 07:27
  • @legends2k It just does a `memcpy` – Boiethios Feb 07 '20 at 08:27
  • With something like `String` involved, how will `memcpy` work, wouldn't it break the pointer's link to heap-allocated string data? – legends2k Feb 07 '20 at 08:57
  • 2
    @legends2k Look at it like this: a `String` consists of exactly three machine words: pointer-to-data, capacity, and length. In your case the `String`, and therefore the three words, are stack-allocated, so moving the object will just copy them verbatim from one stack location to the other. The pointer to heap-allocated data will remain perfectly valid because the data on the heap will remain untouched, and the move is O(1) and extremely efficient. It won't cause a double free because, unlike C++ Rust **won't run the destructor** on the old (moved) object. – user4815162342 Feb 07 '20 at 09:51
  • That last part is crucial :) Thanks for the comment! – legends2k Feb 07 '20 at 09:54
  • @user4815162342 So in Rust both _move_ and _copy_ are `memcpy` with _move_ disallowing original's use. Intelligent, deep copies are delegated to the type author by the `Clone` trait. Is my summarized understanding correct? – legends2k Feb 07 '20 at 09:57
  • 1
    @legends2k *So in Rust both move and copy*... - Rust doesn't really have copy constructors invoked automatically. Values are moved by default and if you want a copy, you have to request it explicitly by calling `clone()`, after which the clone is again moved as usual. The exception to this are objects that implement `Copy`, but those are forbidden from having destructors and are therefore limited to numbers and structures holding them. – user4815162342 Feb 07 '20 at 10:05
  • Sure, but implementation-wise both are `memcpy` under the hood e.g. for `#[derive(Copy)] struct Point { x: f32, y: f32 }`, both move and copy are implemented as bit-by-bit copy. I understand that copying isn't the default and `Copy` adhering types can't have `Drop`. – legends2k Feb 07 '20 at 11:01
  • @legends2k Correct, both are simple bitwise copy (of length known at compile-time) under the hood. – user4815162342 Feb 07 '20 at 12:13
  • I don't get it why "Rust objects aren't heap allocated". I mean if I create a let x = vec!- then surely there is data on the heap. The x itself is realized on the stack, but this isn't any different from a C call like p= (int *) malloc(....); here the pointer p is on the stack, whereas it points to a region in the heap, where data can be put. – P.Jo Aug 25 '23 at 14:28
  • @P.Jo I wrote that because the very first sentence of the OP's question (above the code sample) suggests a mental model where objects exist on the heap, and variables are essentially just pointers. In that model an assignment just makes the variable point to a different object, and as the OP puts it, "the bits of the object stay put". That model is quite correct for Python and Java, but not for Rust and C++, the latter being much more value-oriented. – user4815162342 Aug 25 '23 at 16:15