Move Semantics
This chapter covers move semantics. You will learn the following:
What are move semantics?
What are the different value categories and when should they be used?
What is universal reference T&&?
How and why should std::move be used?
How is a move constructor created? (Rule of Five)
Introduction
Move semantics allows an object under certain conditions to take ownership of some other object's external resources.
Value Categories (glvalue and rvalue)
In C++, every expression has a type and belongs to a specific value category. These are the basic rules for a compiler to follow when creating, copying, and moving objects during expression evaluation.
Here are some C++ expression value categories:
glvalue — Expression that has an identity; it's possible to determine if two expressions refer to the same entity.
rvalue — Expression that can be moved from.
lvalue — Has an identity and can't be moved from.
xvalue — Has an identity and can be moved from.
prvalue — Doesn't have an identity and can be moved from.
The diagram below shows different expression types and the dependencies between them:
Let's look at the lvalue and rvalue in the following example:
size_t x = 0;
x = 1; // x expression is lvalue
size_t foo() { /* ... */ }
foo(); // Result of foo call expr is an rvalue
Any name of variable, function, template parameter object, or data member is an lvalue. It's important to note that it doesn't matter how complex the expression is. As long as it maintains an identity, the expression is an lvalue.
The integer constants are prvalues, like in the code above — the result of a function call.
Functions Returning lvalue and prvalue
Let's imagine a function returning a int value:
int returnValue() {
return 3;
}
In this case, returnValue() returns the temporary number 3, which is a prvalue. Now, we will try to assign the value to it:
returnValue() = 17; // error
We will receive an error: lvalue required as left operand of assignment. That's because we are trying to use the left operand of the assignment on prvalue. But when we change the returnValue() function to return a reference to an already existing memory location, everything will work fine:
int globalValue = 43;
int& returnValue() {
return globalValue;
}
// ...
returnValue() = 17; // works fine
Even though the ability to return an lvalue may not seem intuitive, it can be useful when implementing more advanced functions like overloaded operators.
lvalue-to-prvalue Conversion
An lvalue may be converted to a prvalue. This is totally legal and occurs frequently. Let's look at +operator as an example. According to the C++ standard, it takes two prvalues as arguments and returns a prvalue.
int x = 10, y=20;
int z = x + y;
x and y are lvalues, but the additional operator wants prvalues. How is it possible? Because of an implicit lvalue-to-prvalue conversion. There are many more operators performing similar conversions. But what about the opposite — converting prvalue to lvalue? It is not possible due to the C++ design.
Universal References (&&)
One of the main features related to the rvalues introduced in C++11 was rvalue reference. Usually, the && notation is known as a syntax for rvalue reference. But it is not always true. T&& can hold both lvalue and rvalue references, which is called a universal reference. But remember that && only means a universal reference when type deduction is involved. In other cases, we can assume that it means only an rvalue reference. Let's see it in code. We will start with a universal reference, as the T is deduced.
template<typename T>
void foo(T&& param);
Now, let's move on to an rvalue reference, as there is no type deduction.
void foo(std::string&& param);
Finally, the last thing is to show the concept of prefect forwarding, which is when a universal reference can be propagated, preserving the l-r 'valueness'.
template<typename T>
void foo(T&& param) { /* ... */ }
template<typename T>
void bar(T&& param) {
foo(std::forward<T>(param)); // l or r value depending on the param passed to `bar`
}
In this case, because both functions foo and bar are using a universal reference, foo will receive an l or r value, depending on the param passed to bar.
std::move
Let's start by answering the question: What is std::move?
According to the C++ Reference:
std::move is used to indicate that an object t may be "moved from"
(i.e., allowing the efficient transfer of resources from t to another object).
In other words, it is a way to efficiently transfer contents of an object to another,
leaving the source in a valid but undefined state. When you move a value from a register
or memory location to another place, the value on the source register or memory location
is still there. And more formally, std::move is a C++ Standard Library function
that's defined in the <utility> header. It is used to cast an l-value reference
to an r-value reference, which enables move semantics.
Let's see an example. We will start with a declaration of the function consuming the element.
void consume_element(std::unique_ptr<int> element);
Then, let's declare it and consume using a prepared function and std:move.
std::unique_ptr<int> element = std::make_unique<int>(30);
consume_element(std::move(el));
After those operations, the declared element element is nullptr, as it was moved.
assert(element == nullptr);
Move Constructor and Rule of Five
std::move is actually just a request to move. If the type of the object does not have a move constructor/assign operator defined, the move operation will fall back to a copy. In that case, we will not experience any benefits of using the move operation.
That is why it is important to know how to create a move constructor. At the same time, in C++ we have something called Rule of Five, which is as follows:
If a class requires a user-defined destructor, a user-defined copy constructor, or a user-defined copy assignment operator, it almost certainly requires all three.
Any class for which move semantics are desirable needs to declare the move constructor and the move assignment operator.
Those result in creating five elements:
user-defined destructor
user-defined copy constructor
user-defined copy assignment operator
user-defined move constructor
user-defined move assignment operator
Let's show this with an example. Imagine a class called MoveClass with a private member called str_ptr being char*. To show the Rule of Five, we need to declare the following:
custom destructor
custom copy constructor
custom move constructor
custom copy assignment operator
custom move assignment operator
class MoveClass {
char* str_ptr;
public:
explicit MoveClass(const char* s = "") : str_ptr(nullptr) {
if (s) {
std::size_t size = std::strlen(s) + 1;
str_ptr = new char[size]; // allocate
std::memcpy(str_ptr, s, size); // populate
}
}
// Destructor - we need to deallocate str_ptr
~MoveClass() {
delete[] str_ptr;
}
// Copy constructor - uses explicit constructor, parameter passed is const&
MoveClass(const MoveClass& other)
: MoveClass(other.str_ptr) {}
// Move constructor - uses std::exchange function, parameter passed is &&
MoveClass(MoveClass&& other) noexcept
: std_ptr(std::exchange(other.str_ptr, nullptr)) {}
// Copy assignment operator - uses copy constructor,
// passed parameter similarly to copy constructor is const&
MoveClass& operator=(const MoveClass& other) {
return *this = MoveClass(other);
}
// Move assignment operator - uses std::swap function,
// passed parameter similarly to copy constructor is &&
MoveClass& operator=(MoveClass&& other) noexcept {
std::swap(str_ptr, other.str_ptr);
return *this;
}
};