Move Semantics in Modern C++ (3)

In the previous two articles we looked at how to transfer “move-aware” objects, such as std::string, between variables, and also how to create a Movable class which logs all copy and move operations applied to it in order to gain a clearer picture of what is actually taking place. In this article we’re going to look at how to move objects into different scopes, these being caller and callee (function) scopes.

With std::move under our belt, we may consider that a return to “pass-by-value” for function parameter syntax may be useful. We could, for example, do this:

 #include "Movable.hpp" void f(Movable obj) { std::cout << "Entering f()\n"; obj.print(); std::cout << "Leaving f()\n"; } int main() { Movable m{ "Pass-by-value" }; f(std::move(m)); std::cout << "Leaving main()\n"; } 

Running this program proves that no copying takes place:

 Construct Movable with id 1 and content: Pass-by-value Move-construct Movable with id 2 from id 1 Entering f() 2: Pass-by-value Leaving f() Delete Movable with id 2 Leaving main() Delete Movable with id 1 

We should also note that f() “sees” a different object (id 2) to main() (id 1), and also that the contents of this object are deleted at the return of f(), not main(). We could even achieve the same result with a temporary:

 f(std::move(Movable{ "Pass-by-value" })); 

Leaving responsibility to the caller is not ideal, however, so C++ gives us a shorthand where use of std::move() is mandated, by declaring the function as void f(Movable&& obj) {...}:

 #include "Movable.hpp" void f(Movable&& obj) { std::cout << "Entering f()\n"; obj.print(); std::cout << "Leaving f()\n"; } int main() { Movable m{ "Pass-by-universal-ref" }; f(std::move(m)); std::cout << "Leaving main()\n"; } 

Running this program produces the output:

 Construct Movable with id 1 and content: Pass-by-universal-ref Entering f() 1: Pass-by-universal-ref Leaving f() Leaving main() Delete Movable with id 1 

We should note that no second object is created in this case: Movable&& is truly a “universal reference”. However, there is special treatment of temporaries (which are r-values) in that std::move() is not required:

 f(Movable{ "Pass-by-universal-ref" }); 

So, you might be asking, how to get the behavior of the universal reference version while deleting the object within f()? The answer is to use std::move() within f() itself:

 #include "Movable.hpp" void f(Movable&& ref) { Movable obj = std::move(ref); std::cout << "Entering f()\n"; obj.print(); std::cout << "Leaving f()\n"; } int main() { Movable m{ "Pass-by-universal-ref" }; f(std::move(m)); std::cout << "Leaving main()\n"; } 

The output shown from running this program proves that the contents are deleted as f() returns:

 Construct Movable with id 1 and content: Pass-by-universal-ref Move-construct Movable with id 2 from id 1 Entering f() 2: Pass-by-universal-ref Leaving f() Delete Movable with id 2 Leaving main() Delete Movable with id 1 

In a different scenario, we may want to call a new function g() with a “forwarding reference” on which it invokes perfect forwarding (passing it unmodified in every sense) to the second version of f() above. Here is the complete program:

 #include "Movable.hpp" void f(Movable&& obj) { std::cout << "Entering f()\n"; obj.print(); std::cout << "Leaving f()\n"; } void g(Movable&& fwd) { std::cout << "Entering g()\n"; f(std::forward<decltype(fwd)>(fwd)); std::cout << "Leaving g()\n"; } int main() { Movable m{ "Pass-by-forwarding-ref" }; g(std::move(m)); std::cout << "Leaving main()\n"; } 

The output from running this program shows that even though three different functions are involved, only one object is created:

 Construct Movable with id 1 and content: Pass-by-forwarding-ref Entering g() Entering f() 1: Pass-by-forwarding-ref Leaving f() Leaving g() Leaving main() Delete Movable with id 1 

(The third version of f() could be used instead in order to create a second object, destroyed when f() returns.)

It is important to note that std::forward needs to be provide with the exact reference type(s) (as provided by decltype()), and it is also compatible with template parameter packs. Having seen how to pass (and forward) universal references, a couple of “anti-patterns” should be noted:

  1. Use of const with the universal/r-value/forwarding syntax && is typically not used, unlike with traditional (l-value) references.
  2. Universal references are rarely returned from functions.

To explain the first of these, because the value is no longer part of the caller function, any future modifications to it are unimportant, so const is not needed. The second is explained by the (N)RVO, or “(Named) Return-Value Optimization”, where the return variable (or temporary) is constructed in-place in the callers scope as a compiler optimization. Therefore std::move() is typically not used with function return values.

That just about wraps thing up for this article. We have learned about when std::move() can be used in the context of function parameters, and when std::forward is needed. In the next article in this mini-series we’ll take a look at member function overloading based on “reference-ness”, and discuss reference collapsing.

Leave a comment