What would happen when returning a non-copy struct from a fn?


#1

I’m trying to understand the move semantics of rust, and have a problem about the handling of the return value of a function.

The question is that:

If I have such a struct:

struct SomeType{…}

and such a function that returns above struct:

fn someFunc()->SomeType{
SomeType{…}
}

Then the caller calls the function as

let x=someFunc();

Would the x own the object EXACTLY (have same memory address) the same that I created in the function someFunc?

If not, the struct must be copied, then in language like C++, the behavior of coping of struct can be user-defined in copy constructor, How does rust handle the copy?

If yes, does it mean that rust guarantee return value optimization?

Actually there was such a topic on this issue:

But I failed to find a firm result.

Thanks


#2

In Rust, a move is always bitwise copy (memcpy) and it’s not possible to configure this behavior. To logically copy the object, a-la C++ copy ctor, there’s a Clone trait with a .clone(&self) -> Self method, but Rust will never call .clone implicitly.

Rust does have an RVO optimization, but, unlike C++, it is not observable and does not change behavior.


#3

Oh, yeah, one more missing piece: returning a struct from a function does a move and not a copy.


#4

So semantically speaking, the return value from a fn is actually move to some variable, and the inner mechanism is bitwise copying. If we are lucky enough to have RVO, then no actual copy happens. Is that right?

So that when returning struct like Vec, actually the stack part is copied, and the heap part is truly MOVED (not semantically moved) from the returned struct to the function return value accepter?


#5

So semantically speaking, the return value from a fn is actually move to some variable, and the inner mechanism is bitwise copying. If we are lucky enough to have RVO, then no actual copy happens. Is that right?

Yep.

So that when returning struct like Vec, actually the stack part is copied, and the heap part is truly MOVED (not semantically moved) from the returned struct to the function return value accepter?

I am not sure what meaning do you ascribe to MOVE here :slight_smile: The stack part (len + capancity + ptr to data) is copied, and nothing happens to the heap part at all (though semantically, it becomes the contents of another vector).

I’ll through in another fact, useful for forming mental model of Rust. In Rust, when the vector capacity is doubled, the elements are not moved one by one, which might be the case with C++, but are “memcopied” en mass (Actually, they are realloced, which might be even more efficient then memcpy https://github.com/rust-lang/rust/blob/909b94b5cceb046a7a3aa7134be1a3e25f75fec4/src/liballoc/raw_vec.rs#L312).


#6

What I meant by MOVE is in C++11 move sense, i.e., transfer the ownership to another holder.


#7

Yep, you are 100% correct then!


#8

For me at least you need to separate the concept of an instance from the low level memory.
First memory is allocated then an instance is constructed in the memories bytes. Unlike C if you want uninitialised you have to go out of your way with an unsafe function to get it. Unlike Java the instance has its fields set as you like rather than zero to start with.

Rust move is a mem copy of the bytes to a new location. The old memory may still exit but the only thing you can do is initialise a new instance to it, (move one to it, or leave it to be deallocated.). C++ move on the other hand will at the end call the destructor on the old memory; potentially requiring you to write something more complicated.


#9

Couple of additional notes on top of already mentioned things:

  1. RVO isn’t guaranteed in Rust and NRVO doesn’t exist at all (at the ABI level) right now.
  2. A Copy vs move struct in Rust behaves the same way, with the sole difference being whether the compiler allows you to continue using the original value after the copy/move. It’s a memcpy either way (modulo optimizations).

#10

I believe NRVO is being implemented in rust-lang/rust#46321.


#11

Indeed!

Compared to Rust’s move semantics, what C++ calls move semantics seem more like a “steal semantics”: a new object is created, it steals the important resources from the old one with a customizable piece of logic (such as move constructor); the old one continues to exist, and if nothing else will happen to it, its destructor will be run at the end of its scope.

In Rust, here’s what happens:

On the memory level, the old memory is memcopied to the new memory (no customization is possible). Then the old memory is treated as “logically uninitialized” — that is, unless you put something else in there, you can’t access it, and no destructor will be called.

On the instance level, the value is moved from the old place to the new one. It’s the same value, not a copy (like it would be in C++); the value just changed its memory location. There’s nothing left at the old location (again, we’re not copying the value, we’re moving it), so there’s nothing to run a destructor on.

Yes, if RVO kicks in, there wouldn’t even be a memcopy, but it’s just that, optimization, it doesn’t impact the program logic in any way.