1

I am learning rust currently, and I found some interesting behavior about function returning structure.

Suppose struct User is defined like this.

struct User {
    email: String,
    username: String,
    age: i32,
}

And a function to generate the User struct defined like this.

fn build_user(email: String, username: String, age: i32) -> User {
    let built_user = User {
        email,
        username,
        age,
    };

    println!("&built_user : {:p}", &built_user);
    built_user
}

So, now in function main, I print the pointer of build_user's return value.

fn main() {
    let user1 = build_user(String::from("asd"), String::from("def"), 32);
    println!("&user1 : {:p}", &user1);
}

If you run it you will see two pointers printed out is same!

Since assigning a struct to new variable (either copy or move) will make new struct object on stack, any other assignment like

let user2 = user1;

will result different pointers.

And even more weird thing is that if structure only contains types with copy trait, something like this,

struct User {
    age: i32,
    login_counts: i32,
}

would behave differently and two pointers will not be equal.

I experimented a lot applying small changes to struct and fn, but this explicit case (struct that doesn't implements copy trait / fn returning only struct, not tuple) seems to be the only one behaving differently to normal assignment.

Can anyone explain why it behaves like this? Thanks

  • 1
    Return Value Optimization (RVO)? – canton7 Mar 11 '22 at 09:27
  • Can you please explain what RVO is? Is that something like rvalue from C++? – CodingIsPain Mar 11 '22 at 09:28
  • I suspect the difference is not the presence of Copy elements, but just the size of the struct. I haven't done any tests though – canton7 Mar 11 '22 at 09:29
  • 2
    @canton7: And/Or inlining. Rust will avoid actually mem-moving objects if it can determine that it is not actually needed, even in debug builds. – rodrigo Mar 11 '22 at 09:30
  • See e.g. [here](https://users.rust-lang.org/t/does-rust-have-return-value-optimization/10389/3). The gist is that the caller allocates space for the return value, and passes a pointer to this space to the callee. The callee then initialises the return value directly in this space create by the caller. This avoids the callee needing the allocate the return value on in its own stack frame, only to immediately copy it back to the caller – canton7 Mar 11 '22 at 09:31
  • Then why RVO wouldn't happen in case of tuple? Some thing like - let tuple_value1, user1 = build_user(...); - – CodingIsPain Mar 11 '22 at 09:33
  • Have you got a repro? It does happen [here](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=eb72a8ecedcb2a4922a3cd9dfbfa111b) – canton7 Mar 11 '22 at 09:37
  • https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=66d47c206a7b139cacf48b9ef75ac5ac – CodingIsPain Mar 11 '22 at 09:41
  • See also: [Can Rust optimise away the bit-wise copy during move of an object someday?](https://www.stackoverflow.com/questions/38571270/can-rust-optimise-away-the-bit-wise-copy-during-move-of-an-object-someday) (in this case, LLVM did the nice thing to do) – E_net4 Mar 11 '22 at 09:42
  • @E_net4standswithUkraine My question was that I assume that move will indeed memcpy, but because of RVO (as canton7 answered), it won't memcpy. – CodingIsPain Mar 11 '22 at 09:44
  • You can see [here](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=fc07614aebc9fbd0c97941acf8901657) is that it is doing RVO, but it's moving the `User` into the tuple that's returned, and it's also moving it out during the deconstruction. I don't know enough to say why it's bothering to do either of these. – canton7 Mar 11 '22 at 09:45
  • 1
    The general answer here is that the compiler is allowed to ellide moves if it doesn't change the behaviour of the program. You don't get any guarantees when and if the compiler will do it, but as long as it doesn't change the behaviour of the program, the compiler _might_ do it. – Sven Marnach Mar 11 '22 at 09:46
  • Although, according to [this linked answer](https://stackoverflow.com/questions/38571270/can-rust-optimise-away-the-bit-wise-copy-during-move-of-an-object-someday), it might be being forced to move the `User` around precisely because you're taking the address of various things... – canton7 Mar 11 '22 at 09:50
  • So I basically get the general concept of RVO, but it just seems like exact implementation of it is done by creators of rust or C++, so thanks anyway! – CodingIsPain Mar 11 '22 at 09:54
  • @SvenMarnach I always like to qualify the as-if optimization rule because it can change the behavior of the program (like we see here). The as-if rule pretends you can't see the addresses of values, and pretends that copy/move ctors (in C++) don't have side effects. If you start comparing pointers or putting side effects in your copy/move ctors then you can see behind the as-if veil. – cdhowie Mar 11 '22 at 16:19

0 Answers0