chellathurai
chellathurai

Reputation: 115

How can the memory address of a struct be the same inside a function and after the struct has been returned?

The phrase "when scope exits the values get automatically popped from stack" is repeated many times, but the example I provide here disproves the statement:

fn main() {
    let foo = foobar();
    println!("The address in main {:p}", &foo);
}

fn foobar() -> Employee {
    let emp = Employee {
        company: String::from("xyz"),
        name: String::from("somename"),
        age: 50,
    };
    println!("The address inside func {:p}", &emp);
    emp
}

#[derive(Debug)]
struct Employee {
    name: String,
    company: String,
    age: u32,
}

The output is:

The address inside func 0x7fffc34011e8
The address in main 0x7fffc34011e8

This makes sense. When I use Box to create the struct the address differs as I expected.

  1. If the function returns ownership (move) of the return value to the caller, then after the function execution the memory corresponds to that function gets popped which is not safe, then how is the struct created inside the function accessible even after the function exits?
  2. The same things happens when returning an array. Where are these elements stored in memory, whether in the stack or on the heap?
  3. Will the compiler do escape analysis at compile time and move the values to the heap like Go does?

I'm sure that Employee doesn't implement the Copy trait.

Upvotes: 2

Views: 913

Answers (1)

prog-fh
prog-fh

Reputation: 16900

In many languages, variables are just a convenient means for humans to name some values. Even if on a logical point of view we can assume that there is one specific storage for each specific variable, and we can reason about this in terms of copy, move... it does not imply that these copies and moves physically happen (and notably because of the optimizer). Moreover, when reading various documents about Rust, we often find the term binding instead of variable; this reinforces the idea that we just refer to a value that exists somewhere. It is exactly the same as writing let a=something(); then let b=a;, the again let c=b;... we simply change our mind about the name but no data is actually moved.

When it comes to debugging, the generated code is generally sub-optimal by giving each variable its own storage in order to inspect these variables in memory. This can be misleading about the true nature of the optimised code.

Back to your example, you detected that Rust decided to perform a kind of return-value-optimization (common C++ term nowadays) because it knows that a temporary value must appear in the calling context to provide the result, and this result comes from a local variable inside the function. So, instead of creating two different storages and copying or moving from one to another, it is better to use the same storage: the local variable is stored outside the function (where the result is expected). On the logical point of view it does not change anything but it is much more efficient. And when code inlining comes into play, no one can predict where our variables/values/bindings are actually stored.


Some comments below state that this return-value-optimisation can be counted on since it takes place in the Rust ABI. (I was not aware of that, still a beginner ;^)

Upvotes: 2

Related Questions