1

I have this code:

struct Example {
    x: i32,
    b: Option<Box<Example>>,
}

impl Example {
    fn get_self(&self) -> String {
        format!("{:p}", self)
    }
}

impl Drop for Example {
    fn drop(&mut self) {
        println!("Droppped: {} {:p}", self.x, self);
    }
}

fn take(t: Example) {
    println!("Taken ownership");
}

fn main() {
    let mut myE = Example {
        x: 1,
        b: Some(Box::new(Example { x: 2, b: None })),
    };
    println!(
        "Parent: {} Child: {}",
        myE.get_self(),
        match &myE.b {
            Some(x) => x.get_self(),
            None => String::from("Error"),
        }
    );
    take(myE);
    println!("End!");
}

And this is the output

Parent: 0x7ffdc8031560 Child: 0x556b88bb5ba0
Taken ownership
Droppped: 1 0x7ffdc80314a8
Droppped: 2 0x556b88bb5ba0
End!

Notice how the address of the child is the same address that is dropped once it goes out of scope but once the parent goes out of scope it changes for some reason. What is the reason for that?

I would expect the output to be something like this:

Parent: 0x7ffdc8031560 Child: 0x556b88bb5ba0
Taken ownership
Droppped: 1 0x7ffdc8031560
Droppped: 2 0x556b88bb5ba0
End!

Where the printed address is the same address that gets dropped.

Peter Hall
  • 53,120
  • 14
  • 139
  • 204
Marko Borković
  • 1,884
  • 1
  • 7
  • 22
  • 3
    Why would it be the same? Parent structure is created in stack frame of `main`, but dropped in stack frame of `take`. Child structure, on the other hand, is on the heap and doesn't relocate after being boxed. – Cerberus Aug 05 '21 at 16:15
  • also https://rust-unofficial.github.io/too-many-lists/ linked list IS NOT rust friendly – Stargateur Aug 05 '21 at 16:25
  • 1
    @Cerberus if I understood it correctly would [this diagram](https://imgur.com/a/vxq2y7u) be correct? The parent struct gets copied from the stack frame of `main` to the stack frame of `take` and is made inaccessible from main from that point onward since it has moved. Then when `take` is finished its stack frame is freed causing all the data that is owned by stuff on its stack frame to be freed as well including the child. And because throughout this entire process the child is on the heap its address doesn't change. – Marko Borković Aug 05 '21 at 16:36
  • 2
    @MarkoBorković that not even guaranty, compiler is allow to do whatever it want, the only real fact it's the parent is moved, then compiler can reuse stack space, not reuse stack space or send a HTTPS request to nasa to ask for memory to stock the parent. Whatever it want if it's work. – Stargateur Aug 05 '21 at 16:46
  • @MarkoBorković One way to think of it is that the program behaves _as if_ your diagram were correct. The compiler can introduce further optimizations, such as allocating "stack" data in registers, or completely omitting operations (e.g. when it can calculate some data at compile time or prove that it's not used). The only constraint is that the observable behavior of a program not change. – user4815162342 Aug 05 '21 at 18:50

1 Answers1

2

This is move semantics in action. When you pass something by value, it can be literally moved in memory. Often, for efficiency reasons, the compiler may choose not to move something but you should always assume that any operation that moves a value could physically move it.

The exact behaviour could change with a different Rust version, debug vs release builds, inlining hints, link-time optimisations, target operating system or architecture, or just that you added another line of code that changed the optimal layout of the stack frame. In all cases, the only assumption that you can make is that a moved value might end up in a different place in memory.

For an even simpler example, you don't even need to move the value into another function:

fn main() {
    let mut myE = Example {
        x: 1,
        b: Some(Box::new(Example { x: 2, b: None })),
    };
    println!("Parent: {} Child: {}", myE.get_self(), match &myE.b {
        Some(x) => x.get_self(),
        None => String::from("Error")
    });

    // move the value
    let moved = myE;

    println!("End!");
}

This still moves the data in memory (at least the time when I ran it, in a debug build).

The boxed value is different because it is allocated on the heap. A Box is really just a pointer and only the pointer is moved; the data it points to stays in the same place.

Peter Hall
  • 53,120
  • 14
  • 139
  • 204