C++ move semantics and rvalue references explained

iteo
3 min readMay 7, 2021

--

Even though modern C++ can produce very fast and efficient applications, for many years, one of its weaknesses was 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:

  1. 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)
  2. 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 this 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:

  1. Local variable numbers inside getNumbers function — temporary object
  2. 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:

  1. Local variable numbers inside getNumbers function — temporary object
  2. Right hand side object where getNumbers is called — temporary object
  3. 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.

--

--

iteo

iteo is an international digital product studio founded in Poland, that helps businesses benefit from technology better. Visit us on www.iteo.com