The std::move()
utility function was introduced in C++11. It enables the transfer of resources from one object to another without the cost of deep copying. It is important to understand that std::move()
merely enables the transfer of resources; it does not actually move or transfer resources by itself. std::move()
simply casts an object into an rvalue reference, signaling the intent that the resources of this object can be “moved from” rather than “copied.”
Before we discuss how std::move()
actually works, first we need to understand why we need std::move()
in C++ in the first place. To understand this, we need to build some background.
We have already discussed that std::move()
casts a given object to an rvalue reference. Now, the first question that comes to mind is: What is an rvalue
? What is an lvalue
? And what exactly is an rvalue reference
? Let’s start with the basics:
What is an L-Value? #
When we see an expression in C++, for example:
int x = 10;
Here, x
is an lvalue
because it refers to a persistent object that has a memory address. We can point to this object in memory and even take its address. That is why it is considered an lvalue
. In an expressiona above, variable x
has a name and we can take its address, therefore it is an lvalue.
What is an R-Value? #
In the same expression,
int x = 10;
The value 10
is a temporary value, or you can say an rvalue
. It does not have a specific memory location, and you cannot take its address. That’s why 10
is considered an rvalue in this expression.
Let’s see another example:
int y = x + 5;
In this expression, x + 5
is an rvalue
because it exists temporarily during the evaluation of the expression. It does not have a specific memory location, and you cannot take the address of x + 5
. On the other hand, in the same expression, the variable y
is an lvalue because it has a name and a memory address.
Now that it’s clear what an lvalue and an rvalue are, let’s move on to lvalues references and rvalue references.
lvalues references vs rvalue references #
Before C++11, we already had references, such as when we take a reference to a variable x
:
int x = 5;
int& ref = x;
Here, x
is an lvalue, and ref
is an lvalue reference to x
.
C++11 introduced rvalue references, which allow us to refer to rvalues (temporary values). For example:
int&& rvalueRef = 10;
Here, rvalueRef
is an rvalue reference to the temporary value 10
.
The syntax for rvalue references is T &&
, where T
is the type of the referred object. It allows us to refer to temporary objects and operate on them without making copies.
Why R-Value References are important? #
Before rvalue references, it was not possible to take references to temporary values:
// Error: cannot bind lvalue reference to an rvalue
int& ref = 20;
The above expression will cause a compile-time error because an lvalue reference cannot bind to an rvalue. However, with rvalue references, we can create references to temporary values:
// Valid: rvalue reference binds to the temporary value
int&& rvalueRef = 20;
Now, we can use rvalue and lvalue references to overload functions effectively.
For example, we have a Product
class that has a member variable, a pointer to the product name, and an integer for the product price.
class Product
{
public:
int price_;
char* productName_;
Product(const char* productName, int price);
// Copy constructor
Product(const Product& other) ;
// Move constructor
Product(Product&& other) noexcept;
// Move Assignment Operator
Product& operator=(Product&& other) noexcept;
// Assignment Operator
Product& operator=(const Product& other);
~Product() {};
};
In this Product
class, we have a copy constructor and a move constructor. Now, let us see the syntax and declaration of these two constructors.
// Copy constructor
// Accepts a const lvalue reference as argument
Product(const Product& other) ;
// Move constructor
// Accepts a const rvalue reference as argument
Product(Product&& other) noexcept;
The first, the copy constructor, accepts a constant reference (an lvalue reference) as its parameter. The second, the move constructor, accepts an rvalue reference as its parameter. Here, we have overloaded based on the type of reference:
- In the copy constructor, we take an lvalue reference of the same class object.
- In the move constructor, we take an rvalue reference of the same class object.
As we have already discussed, we can make lvalue references to non-temporary objects only . For example, if we try to copy an existing Product
object to another Product
object like this:
Product obj1("Macbook", 9000);
Product obj2 = obj1;
It will invoke the copy constructor because obj1
is an lvalue, and we are trying to create a copy of it in new Product object obj2
. Since the copy constructor accepts an lvalue reference and we are passing an lvalue i.e. obj1
, therefore the copy constructor will be called here.
The Deep Copy Behavior of Copy Constructor
// Copy Constructor
Product::Product(const Product& other)
{
price_ = other.price_;
productName_ = new char[strlen(other.productName_) + 1];
strcpy(productName_, other.productName_);
std::cout << "Copy constructor called\n";
}
In this case, a deep copy will happen because the Product
class internally has a pointer that points to memory on the heap. We cannot perform a shallow copy of the pointer; we need to deeply copy it. A new memory block will be allocated on the heap inside the copy constructor, and all the data will be copied.
Both objects’ internal member variables (productName
) will then point to separate memory locations. The content at those memory locations will be the same, which is why we end up with two copies of the same content.
Moving Resources using std::move()
#
Suppose there is a requirement where we don’t want to create copies but instead move resources or transfer the internal resources of obj1
to another object, say obj3
. If we write something like this:
Product obj1("Macbook", 9000);
Product obj3 = std::move(obj1);
In this case, the move constructor will be invoked. The move constructor allows us to transfer resources instead of duplicating them. Without the move constructor, the copy constructor would have been called, which would allocate new memory on the heap and copy the contents.
We don’t want this behavior; instead, we want to transfer the resources from obj1
to obj3
. For this purpose, we created another constructor—the move constructor—that accepts an rvalue reference.
Move Constructor Implementation #
The move constructor accepts an rvalue reference, and its implementation is as follows:
- It performs a shallow copy of the pointer from the source object (the one received as a parameter) to the target object (the current instance).
- It then sets the pointer of the source object to
nullptr
.
This effectively transfers ownership of the heap memory to the target object. For example:
// Move constructor
Product::Product(Product&& other) noexcept
: price_(other.price_),
productName_(other.productName_) // Transfer ownership
{
// Nullify the source's pointer
other.productName_ = nullptr;
std::cout << "Move constructor called\n";
}
Product obj1("Macbook", 9000);
Product obj3 = std::move(obj1);
std::cout << obj3.productName_ << std::endl;
if (obj1.productName_ == NULL)
{
std::cout << "obj1.productName_ is NULL" << std::endl;
}
Output:
Move constructor called
Macbook
obj1.productName_ is NULL
Here’s what happens:
- The
other
object has a pointer that points to memory on the heap. - The move constructor copies that pointer to the
productName
pointer of the calling object. - It then sets the
productName
pointer of theother
object tonullptr
.
Now, the calling object’s productName
pointer refers to the same memory on the heap that the other
object previously referred to. The other
object’s productName
pointer no longer points to this memory.
Why Move Constructors Are Efficient ? #
In this case, we are moving resources, which is more efficient than deep copying. The process involves:
- Transferring ownership of the heap memory (by copying the pointer address).
- Avoiding the cost of allocating and copying the memory.
- Resetting the original object’s pointer to ensure it no longer owns the memory.
Whenever the move constructor receives an object, it “moves” its internal resources to the calling object’s pointers, making it an efficient way to handle resource ownership transfer.
Move Constructor vs Copy Constructor #
The move constructor is different from the copy constructor. In the copy constructor, we copy the internal resources, while in the move constructor, we move the internal resources. But how do we invoke the move constructor?
If we write something like:
Product obj3 = obj1;
It will invoke the copy constructor because obj1
is an lvalue. When we try to create a new object from an lvalue, the constructor that accepts an lvalue reference (copy constructor) is called.
However, we want to invoke the constructor that accepts an rvalue reference (the move constructor). To do this, we need to convert the lvalue (obj1
) into an rvalue reference. This can be achieved using static_cast
like this:
Product obj3 = static_cast<Product&&>(obj1);
This will invoke the move constructor, and all the resources of obj1
will be moved to obj3
.
Here, using static_cast<Product&&>
, we converted the lvalue
to an rvalue reference
. This allowed us to invoke the constructor that accepts an rvalue reference, and the internal resources of obj1
got moved to obj3
.
The std::move()
Function
#
Instead of using static_cast<T &&>
with template parameters and &&
, we can use std::move()
directly. The std::move()
function does exactly the same thing: it accepts an object as an argument and casts it to an rvalue reference.
For example:
Product obj1("Macbook", 9000);
Product obj3 = std::move(obj1);
works exactly as,
Product obj1("Macbook", 9000);
Product obj3 = static_cast<Product&&>(obj1)
In this case, std::move()
accepts the lvalue obj1
and converts it into an rvalue reference. Since we are now creating a new object from an rvalue reference, the move constructor will be invoked. Inside the move constructor, the internal resources are transferred.
Output of above code will be,
Move constructor called
Macbook
obj1.productName_ is NULL
Now, if we check the productName
pointer of obj3
, it will display the value (e.g., "Macbook"
). If we try to check the productName
pointer of obj1
, it will be nullptr
because the internal resources were moved.
This is how std::move()
helps us convert an lvalue into an rvalue reference, enabling the use of the move constructor.
Using std::move()
to invoke Move Assignment Operator
#
We can use the std::move()
to convert an lvalue to an rvalue reference while invoking a move assignment operator.
The move assignment operator differs from the move constructor:
- The move constructor is invoked when we are creating a new object by transferring the resources of one object to another.
- The move assignment operator is invoked when we already have two existing objects and want to move the resources of one object to another.
Suppose we have two objects:
Product product1("Macbook", 9000);
Product product2("Dell Laptop", 8800);
If we write:
product1 = product2;
This will invoke the copy assignment operator, which copies the internal resources. After the operation, both product1
and product2
will have separate memory locations, though the content will be the same. If we want to move the internal resources instead of copying them, we need to use std::move()
:
product1 = std::move(product2);
Here’s what happens:
std::move(product2)
convertsproduct2
(an lvalue) into an rvalue reference.- This invokes the move assignment operator, which transfers the internal resources of
product2
toproduct1
.
The code of Move assignment operator for Product class will be like this,
// Move assignment operator
Product& Product::operator=(Product&& other) noexcept
{
if (this != &other)
{
// Free existing resource
delete[] productName_;
// Transfer ownership of resources
price_ = other.price_;
productName_ = other.productName_;
// Nullify the source's pointer
other.productName_ = nullptr;
}
std::cout << "Move assignment operator called\n";
return *this;
}
The move assignment operator typically works like this:
- Perform a shallow copy of the resources:
- Assign the pointer of
product2
toproduct1
.
- Assign the pointer of
- Clean up the existing resources of
product1
:- Deallocate the old memory that
product1
was pointing to.
- Deallocate the old memory that
- Nullify the
productName
pointer ofproduct2
to ensure it no longer owns the memory.
So, for code,
Product product1("Macbook", 9000);
Product product2("Dell Laptop", 8800);
product1 = std::move(product2);
std::cout << product1.productName_ << std::endl;
if (product2.productName_ == NULL)
{
std::cout << "product2.productName_ is NULL" << std::endl;
}
Output will be,
Move assignment operator called
Dell Laptop
product2.productName_ is NULL
Benefits of Move Semantics in STL Containers #
The real benefits of rvalue references and move semantics are evident in STL containers like std::vector
. These containers have move constructors and move assignment operators implemented, allowing them to transfer resources instead of copying them. For example, during resizing, vectors can move their elements to a new memory location instead of copying them. This significantly improves performance. If you want to explore this further, you can refer to our another tutorial : Move Semantics and STL Containers in C++
Summary #
When we pass an object to std::move()
, it converts that into an rvalue reference. This allows the compiler to invoke the overloaded function that accepts a rvalue reference like move constructor or move assignment operator instead of their copy counterparts. Move operations are optimized for efficiency because they transfer ownership of resources (like dynamically allocated memory) from the source object to the destination object, leaving the source in a valid but empty state, instead of duplicating the resources.