8

I was trying to understand how structs behave when returned from methods. There is a section in the nightlies section of the "Rust Book" that said if you used the syntax...

let x = box i_return_a_struct();

.. that there wouldn't be a copy, therefore there is no need to return a pointer. But when I started playing with it, it appears that the box is not needed, unless you need the value to exist on the heap.

#[derive(Debug)]
struct Dummy {
    data: i64,
}

impl Drop for Dummy {
    fn drop(&mut self) {
        println!("{:?} is going out of scope", self as *const Dummy);
    }
}

fn make_dummy(i: i64) -> Dummy {
    Dummy { data: i }
}

fn main() {
    {
        let i = 15i32;
        println!("{:?} is a variable on the stack frame", &i as *const i32);

        let dummy1 = make_dummy(1);
        println!("{:?} was returned and is being used", &dummy1 as *const Dummy);
        let dummy2 = make_dummy(2);
        println!("{:?} was returned and is being used", &dummy2 as *const Dummy);

        let dummy3 = Box::new(make_dummy(3));
        println!("{:?} box was returned and is being used", &(*dummy3) as *const Dummy);
        let dummy4 = Box::new(make_dummy(4));
        println!("{:?} box was returned and is being used", &(*dummy4) as *const Dummy);
    }
    println!("Leaving main");
}

Output:

0x23fb94 is a variable on the stack frame
0x23faf8 was returned and is being used
0x23fa50 was returned and is being used
0x2825030 box was returned and is being used
0x2825040 box was returned and is being used
0x2825040 is going out of scope
0x2825030 is going out of scope
0x23fa50 is going out of scope
0x23faf8 is going out of scope
Leaving main

Do values/structs in return position always get allocated in the parents stack frame or receiving box?

EDIT: PS - is there any guidance in the docs for as to when copy elision will occur in general?

EDIT: Beyond the accepted solution, the following Q+A was enlightening: What are move semantics exactly? Clarified many points for me.

Community
  • 1
  • 1
Rich Henry
  • 1,837
  • 15
  • 25

1 Answers1

3

Maybe it's not clear to me what you don't understand. I think you understand, but maybe you don't know yet :D

Normally, the return value of a function (make_dummy for example) is pushed on the stack. Now suppose you want the object on the heap instead. With the new box syntax the compiler can do some optimization if you want the object on the heap.

Now let's take the example from the book.

let y: Box<Dummy> = box make_dummy(some_dummy);

You may think that in the above example the following happens:

  1. the return value from make_dummy is written into the stack (as normally)
  2. a box is allocated to contain a Dummy object
  3. the Dummy value on the stack is copied in the memory pointer by the box object.

With the old Box::new mechanism this is exactly what would happen. Instead, thanks to the experimental box syntax, this happened:

  1. A box is allocated
  2. The pointer to that box is passed in some way to the make_dummy function (with some compiler magic), so the return value is written directly to the boxed memory [there's no extra copies involving the stack]

I hope it's more clear now.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
eulerdisk
  • 4,299
  • 1
  • 22
  • 21
  • There is no magic, copy ellision is common, it already happens in many C and C++ compilers. This is why i attached a Drop handler to my object, as I would expect to see it fire 2 times for each call if a copy was occurring. Unless I'm mistaken, copy elision always happens in the example in my post. – Rich Henry Jul 19 '15 at 22:59
  • Just want to clarify: `Box::new()` isn't the "old" syntax. It's actually a function added to make it possible to construct a `Box` on stable while the `box` syntax is feature-gated. If/when `box` gets stabilized, `Box::new()` will probably go away, or just remain as another way to get a `Box`. – Austin B Jul 20 '15 at 06:12
  • @RichHenry If a copy was occurring the compiler would have complained about the fact that `Dummy` doesn't implement the `Copy` trait. In case of `Box::new(make_dummy(3))` the value is moved (in the sense of transfer of ownership) into the box not copied, so you don't see extra drop logs. More, `Box::new` is simply a function the compiler cannot optimize the way I described above so there's still all the stuff moving around from the stack to the heap. – eulerdisk Jul 20 '15 at 08:08
  • That does explain why I don't see the drops, as those are a function of compiler bookkeeping. But go back to the not-boxed versions -- I would expect that the compiler is doing return-val optimization and is allocating those values on the caller's stack -- not the callee's, because it sees that no one else could have a handle to the object because it's in return position. Perhaps the `box` syntax can further optimize that to place the object directly on the heap without touching the stack. – Rich Henry Jul 20 '15 at 12:51
  • It would be awesome if someone wrote up some tips or a guide as to when real copy ellision should be expected to occur. C/C++ basically says "it depends" and you have to look at the generated code to be sure. It would be a shame if rust could do no better. – Rich Henry Jul 20 '15 at 12:53
  • @RichHenry Yes, the new box syntax does exactly that optimization, as I said above (the second "2.") That was the "compiler magic" I was talking about. – eulerdisk Jul 20 '15 at 13:00