0

I've been investigating how copy elision behaves when it is not directly assigned to an lvalue and perhaps chained or used down the road, but haven't found any concrete answers.

For starters, I understand that NRVO occurs in the following example, and the return value is directly constructed in the destination variable:

Type MakeType() {
  Type type;
  // ...
  return type;
}

Type a = MakeType();

However, let's say we have another function which takes in a Type as a parameter:

Type MakeComplexType(/*some signature*/ param_type) {
  Type complex_type = param_type
  // ...
  return complex_type;
}

And we call this as follows:

Type t = MakeComplexType(MakeType());
  1. Is it possible to chain copy elision all the way to t?
  2. If not, can we use std::move strategically, perhaps on a function call itself like std::move(MakeType()), to avoid unnecessary copying?
  3. What should the signature of param_type be such that the above assignment to t is the most efficient?
jeanluc
  • 1,608
  • 1
  • 14
  • 28
  • If returned by value, you don't need an extra `std::move` it even hurts performance – JVApen Aug 01 '18 at 20:33
  • https://stackoverflow.com/a/9595610/3783662 this answer explains it pretty well. for your example, the best you can do is in the function, move the input argument into your new local object. thats one move. the rest will be nrvo and copy elision – phön Aug 01 '18 at 21:03
  • `Type complex_type = param_type;` is not a copy elision context, and `std::move(MakeType())` is a pessimization since `MakeType()` is a prvalue – M.M Aug 01 '18 at 21:28
  • There's no way to have `MakeType()` be constructed in `t` for the syntax in your last call – M.M Aug 01 '18 at 21:31

1 Answers1

2

Copy elision is the technique the compiler uses to prevent unneeded copies. Basically, it preallocates memory outside of the function and passes it in to be used. In case of your temporary, it will be on the stack.

Adding std::move to the return type doesn't help. You are already returning by value, so you have already an rvalue. Casting it no an rvalue with std::move should me a no-op. I'm not aware of the details, however, for some cases adding it can hurt performance.

Focusing on 2: Adding std::move to the function call only has a side effect when returned by non-const reference. In those cases, you most likely wrote a bug as the original will be moved away.

For number 3: My favorite is using f(Arg &&a), as this requires all callers to pass rvalue. If performance is less important, for example: you did not find it in profiling. A value argument (some callers can copy) or even const-reference might do (function can't touch argument, so should copy).

As indicated by the comments, the implementation of the function should also write auto result = std::move(a) as your parameter doesn't benefits from NRVO.

Recent versions of Clang have very good warnings about when std::move should be used and when to remove it. I suggest enabling them. GCC might have some similar warnings, however I'm not up to date with it.

In short: your original code is the best version to use and trust your compiler if it has warnings about this.

JVApen
  • 11,008
  • 5
  • 31
  • 67
  • "In short: your original code is the best version to use". I think this is not true. As mentioned in the comments, moving inside the function will replace one copy with a move. Example: http://coliru.stacked-crooked.com/a/a2937e370af4da86 – phön Aug 02 '18 at 05:27
  • The question is how to call the function, not about the function impl. – JVApen Aug 02 '18 at 05:31
  • Oh. Hm. The call side is okay. OP's question 2: "If not, can we use std::move strategically, **perhaps** on a function call itself like std::move(MakeType()), to avoid unnecessary copying?" So i just looked at everything. – phön Aug 02 '18 at 05:39
  • The short answer: doesn't have any effect. Let me update my post – JVApen Aug 02 '18 at 05:50