Introduction #
Even though classic C++ can produce very fast and efficient applications, for many years, one of its weaknesses was the creation of temporary objects. C++98 standard defined a few compiler optimization techniques such as Copy Elision
and Return Value Optimization
which partially solved this problem but the real game-changer was the move
semantics introduced in C++11.
Move semantics #
To understand the move semantics first let’s look at copy semantics. In general, all classes in C++ can be copied using one of special methods:
- Copy constructor
T t1;
T t2(t1);
- Copy assignment operator
T t1, t2;
t2 = t1;
Similarly C++11 defined another two methods in order to allow moving objects instead of copying:
- Move constructor
T t1;
T t2(std::move(t1));
- Move assignment operator
T t1, t2;
t2 = std::move(t1);
In general, move semantics allows us to take an object from the current context and pass it to another one, avoiding copy when the original object is not needed anymore. If we want to move objects, we need to use std::move function, as in the above example.
It is also worth to mention about 2 issues related to these examples:
- What happens with the t1 variable after the move? According to C++ standard variable after the move is in “valid but unspecified state”. It means we can perform operations that does not need preconditions (e.g. assign new object)
- How does std::move work? In fact std::move doesn’t move anything. To find out what std::move really is we need to dig into rvalues.
Lvalues vs. rvalues #
In C++ (unlike the C) a variable can be declared as reference
. Before C++11 reference
could point only to lvalue
(something whose address can be taken):
int counter = 10;
int& counterRef = counter;
Since C++11 reference
can point to lvalue
or rvalue
. rvalue
reference is basically reference to temporary
object (right-hand side of an assignment expression), e.g.:
int&& counterRef = 10;
The role of rvalue references in move semantics #
As mentioned earlier, there are 4 special methods for handling copy/move operations. Let’s look at their definitions:
Class Point
{
…
Point (const Point& point); //copy constructor
Point& operator(const Point& point); //copy assigment operator
Point(Point&& point); //move constructor
Point& operator=(Point&& point); //move assigment operator
…
}
As we can see copy operations take lvalue-reference
while move operations take rvalue-reference
, so the object is being copied or moved depending on the reference type. And this is what std::move
function does — it just converts lvalue-reference
to rvalue-reference
.
When to use move semantics #
When method takes rvalue as parameter, we can pass rvalue reference (reference to temporary object) but also temporary object itself:
- 100
- “temp”
- Point()
It is a good practice to create overloads for methods takings lvalues and rvalues e.g. a few STL containers have two push_back methods:
void push_back(const T& obj);
void push_back(T&& obj);
It allows us to create copy (if object is still needed in current context) or move (if object is not needed):
std::vector<Point> points;
Point point1, point2;
points.push_back(point1); //lvalue
points.push_back(std::move(point2)); //rvalue
This is typical usage of move semantics.
When not to use move semantics #
Common mistake made by developers is using std::move when the local variable is returned from the function.
std::vector<int> getNumbers() const
{
std::vector<int> numbers = {1,2,3};
return std::move(numbers);
}
auto numbers = getNumbers();
In this case there are 2 objects created:
- Local variable numbers inside getNumbers function — temporary object
- Left hand side object where getNumbers is called — this object is created using move constructor
The issue here is that the compiler by default uses an optimization technique called RVO (Return Value Optimization) in order to avoid copies of temporary objects. Let’s remove std::move from code:
std::vector<int> getNumbers() const
{
std::vector<int> numbers = {1,2,3};
return numbers;
}
Without RVO there are 3 vector instances created:
- Local variable numbers inside getNumbers function — temporary object
- Right hand side object where getNumbers is called — temporary object
- Left hand side object where getNumbers is called
But with RVO there is only one instance created.
Conclusion #
Move semantics is a powerful technique helping avoid unnecessary copies of temporary objects. To take full advantage of it, it’s worth remembering that modern compilers also optimize code in some cases and can do it better than using std::move
.