Mark Mark - 1 month ago 16
C++ Question

Pass by value vs pass by rvalue reference

When should I declare my function as:

void foo(Widget w);


as opposed to

void foo(Widget&& w);
?

Assume this is the only overload (as in, I pick one or the other, not both, and no other overloads). No templates involved. Assume that the function
foo
requires ownership of the
Widget
(e.g.
const Widget&
is not part of this discussion). I'm not interested in any answer outside the scope of these circumstances. See addendum at end of post for why these constraints are part of the question.

The primary difference that my colleagues and I can come up with is that the rvalue reference parameter forces you to be explicit about copies. The caller is responsible for making an explicit copy and then passing it in with
std::move
when you want a copy. In the pass by value case, the cost of the copy is hidden:

//If foo is a pass by value function, calling + making a copy:
Widget x{};
foo(x); //Implicit copy
//Not shown: continues to use x locally

//If foo is a pass by rvalue reference function, calling + making a copy:
Widget x{};
//foo(x); //This would be a compiler error
auto copy = x; //Explicit copy
foo(std::move(copy));
//Not shown: continues to use x locally


Other than that difference. Other than forcing people to be explicit about copying and changing how much syntactic sugar you get when calling the function, how else are these different? What do they say differently about the interface? Are they more or less efficient than one another?

Other things that my colleagues and I have already thought of:


  • The rvalue reference parameter means that you may move the argument, but does not mandate it. It is possible that the argument you passed in at the call site will be in its original state afterwards. It's also possible the function would eat/change the argument without ever calling a move constructor but assume that because it was an rvalue reference, the caller relinquished control. Pass by value, if you move into it, you must assume that a move happened; there's no choice.

  • Assuming no elisions, a single move constructor call is eliminated with pass by rvalue.

  • The compiler has better opportunity to elide copies/moves with pass by value. Can anyone substantiate this claim? Preferably with a link to gcc.godbolt.org showing optimized generated code from gcc/clang rather than a line in the standard. My attempt at showing this was probably not able to successfully isolate the behavior: https://godbolt.org/g/4yomtt



Addendum: why am I constraining this problem so much?


  • No overloads - if there were other overloads, this would devolve into a discussion of pass by value vs a set of overloads that include both const reference and rvalue reference, at which point the set of overloads is obviously more efficient and wins. This is well known, and therefore not interesting.

  • No templates - I'm not interested in how forwarding references fit into the picture. If you have a forwarding reference, you call std::forward anyway. The goal with a forwarding reference is to pass things as you received them. Copies aren't relevant because you just pass an lvalue instead. It's well known, and not interesting.

  • foo
    requires ownership of
    Widget
    (aka no
    const Widget&
    ) - We're not talking about read-only functions. If the function was read-only or didn't need to own or extend the lifetime of the
    Widget
    , then the answer trivially becomes
    const Widget&
    , which again, is well known, and not interesting. I also refer you to why we don't want to talk about overloads.


Answer

The rvalue reference parameter forces you to be explicit about copies.

Yes, pass-by-rvalue-reference got a point.

The rvalue reference parameter means that you may move the argument, but does not mandate it.

Yes, pass-by-value got a point.

For move-only types (as std::unique_ptr), pass-by-value seems to be the norm (mostly for your second point, and first point is not applicable anyway).

For types which has both copy/move (as std::shared_ptr), we have the choice of the coherency with previous types or force to be explicit on copy.

Unless you want to warranty there is no unwanted copy, I would use pass-by-value for coherency.