kyles
kyles

Reputation: 3

What happens in memory when ownership transfer happens

Im struggling to fully grasp what happens to the memory part during ownership transfer. if a value is owned by one variable and transferred to another, what happens to the memory in the original variable's stack frame after the transfer

As an example, t is out of scope, but test still works. Where is Test(1) stored, and how is it still valid after the block ends if its created in the stack?

struct Test(u8);

impl Drop for Test {
   fn drop(&mut self) {
      println!("{} is dropped", self.0);
  }
}

fn main() {
   let test;
   {
       println!("step 1");
       let t = Test(1);
       println!("step 2");
       test = t;
   } 
   println!("{}", test.0);
}

output:

step 1
step 2
1
1 is dropped

When an owner goes out of scope, Rust automatically clean-up the data associated with that owner by reclaiming memory. Here's an example that seems to contradict it:

fn main() {
   println!("step 1");
   let test = create_struct();
   println!("step 2");
   println!("{}", test.0);
}

pub fn create_struct() -> Test {
   Test(1)
}

output:

step 1 
step 2 
1 
1 is dropped 

If memory is reclaimed when an owner goes out of scope, how does returning a value from a function prevent this?

Judging from the output of the first example, it's not copied because only one instance of Test was dropped, also that means the stack memory wasn't reclaimed in the inner block or what is happening here?

Upvotes: 0

Views: 101

Answers (2)

Jmb
Jmb

Reputation: 23443

Disregarding compiler optimizations, here's how the stack may look conceptually at various points in your examples:

Example 1

struct Test(u8);

impl Drop for Test {
   fn drop(&mut self) {
      println!("{} is dropped", self.0);
  }
}

fn main() {
   // Stack:
   // +-----+
   // | xxx | test
   // +-----+
   // | xxx | t
   // +-----+
   let test;
   {
       println!("step 1");
       let t = Test(1);
       // Stack:
       // +-----+
       // | xxx | test
       // +-----+
       // |  1  | t
       // +-----+
       println!("step 2");
       test = t;
       // Stack:
       // +-----+
       // |  1  | test
       // +-----+
       // | xxx | t (in practice the value is still "1", but
       // +-----+    this is now irrelevant and ignored)
   } 
   println!("{}", test.0);
   // Stack:
   // +-----+
   // |  1  | test
   // +-----+
   // | xxx | t
   // +-----+

   // - `drop (test)` is called implicitly here.
   // - `drop (t)` isn't called (and indeed cannot be called) since
   //   the corresponding space is considered unused.
}

Note that space is allocated on the stack only once at the beginning of the function and freed all at once when the function returns, even if some parts of the space may be unused at some points in time (denoted by xxx in the schematics above).

Note also that the optimizer may decide to merge the two allocations so that test and t have the same address and there will be no need to copy anything on the test = t line.

Example 2:

fn main() {
   // Stack:
   // +-----+
   // | xxx | test
   // +-----+
   println!("step 1");
   let test = create_struct();
   // Stack:
   // +-----+
   // |  1  | test
   // +-----+
   println!("step 2");
   println!("{}", test.0);
   // `drop (test)` is called implicitly here.
}

pub fn create_struct() -> Test {
   // Stack:
   // +-------+
   // | xxxxx | test (from the `main` stack frame)
   // +-------+
   // |  ptr  | code address from which the function was called
   // +-------+
   // | &test | address where the return value must be stored
   // +-------+
   Test(1)
   // Stack:
   // +-------+
   // |   1   | test (from the `main` stack frame)
   // +-------+
   // |  ptr  | code address from which the function was called
   // +-------+
   // | &test | address where the return value must be stored
   // +-------+
}

Note that the function calling convention is unspecified. I chose to show a convention where the caller passes the address of the return value as a hidden argument to the function, and where that argument is passed on the stack. Depending on the size of the return value, the compiler may also choose to:

  • Pass the address of the return value in a register instead of the stack
  • Allocate some space on the stack when calling a function so that the function can put the return value there, then move the return value to its final location after the function returns (similar to what I showed in example 1).
  • Put the return value in a register and let the caller decide where to store it.

Upvotes: 0

user4815162342
user4815162342

Reputation: 155366

Disclaimer: what actually happens in memory is difficult to pinpoint due to optimizations. For example, a variable doesn't need to be stored in memory at all, it could be in a CPU register. The remainder of this answer assumes that the variables are stored in memory in the "obvious" way.

if a value is owned by one variable and transferred to another, what happens to the memory in the original variable's stack frame after the transfer

It remains unused (or gets reused for storing other variables, with optimization).

As an example, t is out of scope, but test still works. Where is Test(1) stored, and how is it still valid after the block ends if its created in the stack?

It is first stored in t, then moved (bitwise-copied) to test. After the move, the memory in t is no longer relevant.

If memory is reclaimed when an owner goes out of scope, how does returning a value from a function prevent this?

It prevents this by moving the value to another location. If you just called create_struct(); without assigning the value to anything, it would indeed get dropped. By assigning it to a variable, you effectively extend its life.

Judging from the output of the first example, it's not copied because only one instance of Test was dropped

That's correct - it wasn't copied, it was moved. Unlike C++, Rust doesn't do implicit copies, you have to call .clone() explicitly, and then that value is moved.

also that means the stack memory wasn't reclaimed in the inner block or what is happening here?

The stack memory gets reclaimed once the function returns. Before that, moving or dropping the value doesn't reclaim the memory due to how stack works.

Upvotes: 0

Related Questions