Fundamentals 21 min read

Mastering C++ Move Semantics: Rvalue References and std::move Explained

This article explains C++ move semantics, covering the concepts of lvalues and rvalues, the syntax and rules of rvalue references, how std::move converts lvalues to rvalues, and demonstrates practical applications such as custom class move constructors, STL container optimizations, and return value optimization.

Deepin Linux
Deepin Linux
Deepin Linux
Mastering C++ Move Semantics: Rvalue References and std::move Explained

In the world of C++ programming, object passing and resource management are key areas for performance optimization. Traditional C++ often copies objects when passing them, which can cause significant overhead for large objects.

Lvalue and Rvalue Overview

Before diving into rvalue references and std::move , we must understand the concepts of lvalues (Lvalue) and rvalues (Rvalue) in C++. Lvalues have a stable identity and can be addressed, while rvalues are temporary values without a name.

1.1 Lvalues: Stable Entities

An lvalue is an expression with a persistent storage location that can appear on the left side of an assignment. For example:

<code>int num = 10;
num = 20; // num is an lvalue and can be assigned
int* ptr = &amp;num; // address of num can be taken</code>

Array elements and function-returned lvalue references are also lvalues:

<code>int arr[5] = {1, 2, 3, 4, 5};
arr[2] = 100; // arr[2] is an lvalue</code>

1.2 Rvalues: Temporary Guests

Rvalues are temporary expressions that cannot be addressed and can only appear on the right side of an assignment. Examples include literal constants and temporary results:

<code>int result = 1 + 2; // 1+2 is an rvalue
int num = 100; // 100 is an rvalue used to initialize num</code>

A function returning a temporary value also yields an rvalue:

<code>int getValue() {
    return 42;
}
int num = getValue(); // getValue() returns an rvalue</code>

1.3 Special Cases and Identification Tricks

When a function returns a local variable, the return value is an rvalue; returning a reference to a global or static variable yields an lvalue. A simple way to distinguish is to try taking the address: if you can, it is an lvalue; otherwise, it is an rvalue.

<code>int a = 10;
int* ptr1 = &amp;a; // a is an lvalue, addressable
int* ptr2 = &amp;(a + 1); // error, a+1 is an rvalue</code>

Rvalue References: References Born for Rvalues

Introduced in C++11, rvalue references provide a dedicated channel for handling temporary objects efficiently.

2.1 Syntax and Binding Rules

An rvalue reference is declared by appending &amp;&amp; to a type. It can only bind to rvalues:

<code>int num = 10;
int&amp;&amp; r1 = num; // error, cannot bind to lvalue
int&amp;&amp; r2 = 20 + 30; // OK, binds to rvalue</code>

2.2 Differences Between Rvalue and Lvalue References

Lvalue references bind to lvalues, while rvalue references bind to rvalues. Both can modify the bound object, but a const lvalue reference can bind to an rvalue without allowing modification.

Typical usage:

<code>// Lvalue reference example
void processLvalue(int&amp; value) {
    value *= 2;
}
// Rvalue reference example
void processRvalue(int&amp;&amp; value) {
    value += 10;
}
int main() {
    int num = 5;
    processLvalue(num); // modifies num
    processRvalue(10);   // modifies temporary 10
    return 0;
}</code>

2.3 Lifetime and Use Cases

An rvalue reference can extend the lifetime of a temporary object to match its own lifetime, which is useful when returning temporary objects from functions.

The most common use case is move semantics: transferring resources from a temporary object to another without copying.

std::move: Converting Lvalues to Rvalues

std::move is a utility that casts an lvalue to an rvalue reference, enabling move semantics.

3.1 How std::move Works

<code>template &lt;typename T&gt;
typename std::remove_reference&lt;T&gt;::type&amp;&amp; move(T&amp;&amp; t) noexcept {
    return static_cast&lt;typename std::remove_reference&lt;T&gt;::type&amp;&amp;&gt;(t);
}</code>

The template accepts any argument (lvalue or rvalue). If an lvalue is passed, T becomes an lvalue reference type; remove_reference strips the reference, and static_cast converts it to an rvalue reference.

3.2 Usage and Caveats

Example:

<code>std::string str = "Hello, World!";
std::vector&lt;std::string&gt; vec;
vec.push_back(std::move(str));</code>

After std::move(str) , the string’s resources are moved into the vector, avoiding a copy. The original str remains in a valid but unspecified state and should not be used for its previous value.

3.3 Relationship with Move Semantics

Move semantics rely on move constructors and move assignment operators. std::move provides the rvalue reference that triggers these functions. For a class with a move constructor:

<code>class MyClass {
private:
    int* data;
    int size;
public:
    MyClass(int s) : size(s) { data = new int[s]; }
    // Move constructor
    MyClass(MyClass&amp;&amp; other) noexcept : data(other.data), size(other.size) {
        other.data = nullptr;
        other.size = 0;
    }
    // Move assignment operator
    MyClass&amp; operator=(MyClass&amp;&amp; other) noexcept {
        if (this != &amp;other) {
            delete[] data;
            data = other.data;
            size = other.size;
            other.data = nullptr;
            other.size = 0;
        }
        return *this;
    }
    ~MyClass() { delete[] data; }
};
MyClass obj1(10);
MyClass obj2(std::move(obj1)); // invokes move constructor</code>

Practical Exercises: Applying Rvalue References and std::move

4.1 In Custom Classes

Consider a dynamic array class. The traditional copy constructor and copy assignment perform deep copies, which are costly for large arrays.

<code>class MyDynamicArray {
private:
    int* data;
    int size;
public:
    MyDynamicArray(int s) : size(s) { data = new int[s]; }
    // Copy constructor
    MyDynamicArray(const MyDynamicArray&amp; other) : size(other.size) {
        data = new int[size];
        for (int i = 0; i &lt; size; ++i) data[i] = other.data[i];
        std::cout &lt;&lt; "Copy constructor called" &lt;&lt; std::endl;
    }
    // Copy assignment
    MyDynamicArray&amp; operator=(const MyDynamicArray&amp; other) {
        if (this != &amp;other) {
            delete[] data;
            size = other.size;
            data = new int[size];
            for (int i = 0; i &lt; size; ++i) data[i] = other.data[i];
        }
        std::cout &lt;&lt; "Copy assignment operator called" &lt;&lt; std::endl;
        return *this;
    }
    ~MyDynamicArray() { delete[] data; }
};</code>

By adding move constructor and move assignment, we can transfer ownership of the internal buffer instead of copying:

<code>class MyDynamicArray {
    // ... previous members ...
    // Move constructor
    MyDynamicArray(MyDynamicArray&amp;&amp; other) noexcept : data(other.data), size(other.size) {
        other.data = nullptr;
        other.size = 0;
        std::cout &lt;&lt; "Move constructor called" &lt;&lt; std::endl;
    }
    // Move assignment
    MyDynamicArray&amp; operator=(MyDynamicArray&amp;&amp; other) noexcept {
        if (this != &amp;other) {
            delete[] data;
            data = other.data;
            size = other.size;
            other.data = nullptr;
            other.size = 0;
        }
        std::cout &lt;&lt; "Move assignment operator called" &lt;&lt; std::endl;
        return *this;
    }
};</code>

4.2 In STL Containers

When inserting into std::vector , using std::move avoids copying large objects:

<code>std::vector&lt;std::string&gt; vec;
std::string str = "Hello";
vec.push_back(std::move(str)); // moves str into the vector</code>

Erasing elements also benefits from move semantics if the stored type implements a move constructor.

4.3 In Function Return Value Optimization

Without move semantics, returning a local object incurs copy construction:

<code>MyDynamicArray createArray() {
    MyDynamicArray arr(5);
    return arr; // copy constructor called
}
MyDynamicArray obj = createArray();</code>

Using std::move (or relying on compiler RVO) triggers the move constructor, transferring resources efficiently:

<code>MyDynamicArray createArray() {
    MyDynamicArray arr(5);
    return std::move(arr); // move constructor called
}
MyDynamicArray obj = createArray();</code>

Thus, rvalue references and std::move enable high‑performance object handling throughout C++ code.

Performance OptimizationC++move semanticsrvalue referencesstd::move
Deepin Linux
Written by

Deepin Linux

Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.