Contents
After introducing the concept of move semantics and describing how move constructors and move assignment operators work, I’ll wrap up my discussion of the topic with my answer to one question: When should I use rvalue references as function parameters?
Sinks and sources revisited
Let’s have a look again at the code from the move semantics post:
string source() { return "Have you seen my bear Tibbers?"; } void sink(string&& rvalueString) { rvalueString.append("\n"); std::cout << rvalueString; } int main() { sink(source()); //OK auto s = source(); sink(string{s}); //creates a temporary copy }
We haven’t yet cleared up how we can use `sink` equally well for temporary objects that we want to be moved into the function and normal objects of which we want to pass a copy into the function. In the code above we have to manually create the temporary copy of `s`, which is not very clean.
First try: overload the function
We can solve the problem relatively simply if we overload `sink` with a second version that takes its parameter by const reference and creates the temporary copy, so our call looks nice and clean again:
void sink(string&& rvalueString) { rvalueString.append("\n"); std::cout << rvalueString; } void sink(string const& toBeCopied) { sink(string{toBeCopied}); } int main() { sink(source()); //OK - calls the first overload auto s = source(); sink(s); //calls the second overload }
But while the calling code looks better now, the helper overload still looks clumsy. In addition, this approach does not scale. Imagine a function that is the sink for not one, but two parameters. We’d have to provide four overloads:
void doubleSink(string&& rvalueString1, string&& rvalueString2); void doubleSink(string const& toBeCopied1, string&& rvalueString2); void doubleSink(string&& rvalueString1, string const& toBeCopied2); void doubleSink(string const& toBeCopied1, string const& toBeCopied2);
Three parameters? Eight overloads. And so on. Ugh….
Second try: call by value
We know, however, that we have the possibility to construct objects as copies or by moving the originals, depending on the constructor arguments. It will be no surprise to you that the standard library classes, including `std::string`, have implemented the move operations where sensible.
With that knowledge, how about just passing the argument to `sink` by value?
void sink(string aString) { aString.append("\n"); std::cout << aString; }
`aString` will now be an object, constructed from whatever we pass to the function.
int main() { sink(source()); //passing an rvalue auto s = source(); sink(s); //passing an lvalue }
In the first call, `aString` will be move-constructed from the rvalue returned from the call to `source`. It’s not the same as the original, where we could work directly on that rvalue, but it’s not too bad either, because the move will basically rip the guts out of the rvalue and reset it, so the net operation will be updating a handful of pointers and bookkeeping values, which is not costly.
The second call is basically the same as before. Instead of creating a temporary copy of `s` before we call the function it is created during the function call. The rest is identical.
One of the most common uses cases for this technique are constructors. They tend to have more parameters than ordinary functions, and many of those parameters just take the bits and pieces of which the object will be constructed.
What about assignment, e.g. in setters?
Assignment can be tricky. It’s not a pure value sink, because there is one previous value and one new value, and the new value might not necessarily be a temporary.
Even if the new value is a temporary, it depends on the type if the Right Thing is to just discard the previous value and move-assign the temporary to the member, or if it’s better to perform an actual copy.
The key here is that for some types that have to allocate memory, like `std::string` and `std::vector`, move assignment is not so much a time saver as move construction, basically because if the object already has enough memory allocated, the copy is not so costly.
That means, the real benefits of moving that kind of object largely resides in the move construction, not in the move assignment. So, unless you are dealing with objects that are really costly to assign, I would not sweat the special cases where move assignment may or may not give you a performance boost.
After all, it’s better to have clean and simple code than quenching a tiny bit of probably not needed performance out of it by complicating it.
Unless you construct new objects out of function parameters, prefer call by const reference. For constructors, consider passing arguments by value, if they get forwarded to construct member objects.
Conclusion
Move operations can relieve your code from needless allocations of memory and other resources. However, try to not overuse them and don’t prematurely optimize for rvalues.
Permalink
HI, nice post as always, but I got a bit confused by the last point about assignment.
I’m not sure I understand how the capability to reuse storage of, say, vector or string, would make a difference in deciding if its worth implementing an rvalue reference overload of the assignment operator.
Unless you are thinking about the eager deallocation of pre-existing storage in the destination (as a consequence of moving from the source) that wouldn’t happen when simply copying a “shorter” (i.e. using less amount of storage than the destination) source?
Or am I completely misreading what you meant?
Thanks,
Andrea.
Permalink
Hi Andrea, thanks for tuning in. Yes, it has to do with the eager deallocation. There’s a talk from Herb Sutter including some performance analysis of rvalue assignment of strings. I’ll link it as soon as I’m back from the States and have access to something bigger than my cellphone 😉
Permalink
Ok, so I was not completely off-track, and I think I’ve seen the video you’re speaking about, but I’d be grateful if you could post the link when you can, as I can’t remember which one it is (maybe last ccpcon 2014? Or GoingNative?).
I’d really like to have a second look at it.
Thanks and have a safe return home.