What is `std::move()` in C++

What is `std::move()` in C++

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:

  1. 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).
  2. 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 the other object to nullptr.

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) converts product2 (an lvalue) into an rvalue reference.
  • This invokes the move assignment operator, which transfers the internal resources of product2 to product1.

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:

  1. Perform a shallow copy of the resources:
    • Assign the pointer of product2 to product1.
  2. Clean up the existing resources of product1:
    • Deallocate the old memory that product1 was pointing to.
  3. Nullify the productName pointer of product2 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.

Author: Varun

A Software Developer with 20 Years of Experience in C/C++.