Skip to content

Wiki

Rvalue references and move semantics

About 21 min read

Existing English translations were reused. 2 current source blocks remain in Simplified Chinese, without triggering paid translation.

1.What problem does this section solve?

Consider the following scenario:

std::vector<int> create_large_vector()
{
    std::vector<int> v(1000000);
    // ... 填充数据 ...
    return v;  // C++98 中会拷贝整个 vector!(非常慢)
}

In C++98, when a function returns a large object, adeep copyoccurs, resulting in poor performance. C++11 introduced move semantics, which changes "copying someone else's data" into "stealing someone else's data," greatly improving performance.

2.What is this feature?

  • lvalue : An object that has a name and whose address can be taken. For example, variables x、arr[0]。
  • rvalue : A temporary object that is about to be destroyed. For example, 42、x + y、std::move(x)。
  • rvalue reference T&& : A reference bound to an rvalue.
  • Move semantics : Avoid unnecessary deep copies by "stealing" the resources of an rvalue instead of copying them.
  • std::move : Converts an lvalue to an rvalue reference, telling the compiler, "I will not use this object again, so you can steal its resources."

3.C++ Standard Edition

C++11

4.Required header files

#include <utility>  // for std::move, std::forward

5.Basic syntax

int&& rref = 42;           // 右值引用绑定到临时值
std::string&& sr = s1 + s2;  // 绑定到表达式结果

// std::move:把左值转为右值
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1);  // v1 的数据被"偷"到 v2,v1 变成空

6.Lvalue vs Rvalue Cheat Sheet

表达式 左值/右值 说明
int x = 5; x 是左值 有名字,可取地址
42 右值 字面量
x + y 右值 临时结果
f() 返回非引用 右值 临时对象
f() 返回引用 左值 引用是别名
std::move(x) 右值(将亡值) 强制转为右值引用

7.Example code

7.1.Example 1: Copy vs. Move — Why Move Semantics Are Needed

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v1 = {1, 2, 3, 4, 5};

    // 拷贝(两份独立的数据)
    std::vector<int> v2 = v1;
    std::cout << "after copy:\n";
    std::cout << "  v1 size = " << v1.size() << "\n";  // 5
    std::cout << "  v2 size = " << v2.size() << "\n";  // 5

    // 移动(v1 的数据被"偷"到 v3,v1 变成空)
    std::vector<int> v3 = std::move(v1);
    std::cout << "after move:\n";
    std::cout << "  v1 size = " << v1.size() << "\n";  // 0
    std::cout << "  v3 size = " << v3.size() << "\n";  // 5

    return 0;
}

Running result:

after copy:
  v1 size = 5
  v2 size = 5
after move:
  v1 size = 0
  v3 size = 5

7.2.Example 2: Based on Example 1, a custom class's move constructor

#include <algorithm> // for std::copy
#include <iostream>
#include <string>
#include <utility>  // for std::move

class Buffer
{
    int* data;
    size_t size;

public:
    // 构造函数
    Buffer(size_t n) : data(new int[n]), size(n)
    {
        std::cout << "Constructor: allocated " << n << " ints\n";
    }

    // 拷贝构造函数(深拷贝)
    Buffer(const Buffer& other) : data(new int[other.size]), size(other.size)
    {
        std::copy(other.data, other.data + size, data);
        std::cout << "Copy constructor: deep copied " << size << " ints\n";
    }

    // 移动构造函数(偷数据)
    Buffer(Buffer&& other) noexcept
        : data(other.data), size(other.size)
    {
        other.data = nullptr;  // 让原对象安全析构
        other.size = 0;
        std::cout << "Move constructor: stole " << size << " ints\n";
    }

    ~Buffer()
    {
        delete[] data;
        std::cout << "Destructor\n";
    }

    size_t get_size() const { return size; }
};

int main()
{
    Buffer buf1(1000);

    // 拷贝:会触发深拷贝
    Buffer buf2 = buf1;
    std::cout << "buf1 size after copy: " << buf1.get_size() << "\n";

    // 移动:数据被偷走,没有深拷贝!
    Buffer buf3 = std::move(buf2);
    std::cout << "buf2 size after move: " << buf2.get_size() << "\n";
    std::cout << "buf3 size after move: " << buf3.get_size() << "\n";

    return 0;
}

Running result:

Constructor: allocated 1000 ints
Copy constructor: deep copied 1000 ints
buf1 size after copy: 1000
Move constructor: stole 1000 ints
buf2 size after move: 0
buf3 size after move: 1000
Destructor
Destructor
Destructor

7.2.1.Detailed explanation of Buffer examples:std::copy、std::move and object initialization

There are mainly three points in Example 2 that are most easily confused:

  1. std::copy What exactly was copied;
  2. std::move What exactly was done;
  3. Why do both Buffer b = a; and Buffer b(a); invoke the copy constructor?

The following describes each of the Buffer classes one by one.

7.2.1.1.std::copy: Actually duplicate a segment of elements

std::copy defined in <algorithm> In common form, it can be simplified as:

template<class InputIt, class OutputIt>
OutputIt std::copy(InputIt first, InputIt last, OutputIt destination);

The meanings of the three parameters are as follows:

Parameter Meaning
first Start position of the source data
last End position of the source data, exclusive
destination Start position of the target region

The code in the example is:

std::copy(other.data, other.data + size, data);

It can be understood as:

Starting from other.data, copy until before other.data + size, and write the result to the memory pointed to by data.

Suppose size == 3 which is roughly equivalent to:

data[0] = other.data[0];
data[1] = other.data[1];
data[2] = other.data[2];

It can also be written manually as a loop:

for (size_t i = 0; i < size; ++i)
{
    data[i] = other.data[i];
}

std::copy returns the position immediately after the end of the destination range copy. Here, size elements were copied, therefore:

int* end = std::copy(other.data, other.data + size, data);

// 此时 end 等于 data + size

In this example, this position does not need to be used further, so the return value is directly ignored.

Note that std::copy does not automatically allocate memory for the destination region. Before calling it, data must already point to sufficiently large valid memory. In this example, the allocation has already been completed in the member initialization list:

data(new int[other.size])
7.2.1.2.std::move:itself does not move data

std::move is defined in <utility>. Its purpose is neither to copy data nor to move data, but to convert an expression into a form that can bind to an rvalue reference.

Its function template can be simplified and understood as:

template<class T>
std::remove_reference_t<T>&& std::move(T&& value) noexcept;

For the following line of code:

std::move(buf2)

The parameter is buf2, and the return result can be simply understood as a Buffer&&. It is equivalent to telling the compiler:

buf2 The original resources can be transferred, and there is no need to continue retaining the original content.

However, std::move(buf2) this expression itself does not modify buf2. It is the subsequently invoked move constructor or move assignment operator that actually transfers the resource.

For example:

Buffer buf3 = std::move(buf2);

std::move(buf2) Make the right-hand expression match:

Buffer(Buffer&& other) noexcept;

Thus the compiler calls the move constructor, and the move constructor truly takes over the pointer:

Buffer(Buffer&& other) noexcept
    : data(other.data), size(other.size)
{
    other.data = nullptr;
    other.size = 0;
}

Therefore, their responsibilities can be recorded as:

std::copy:真正复制元素
std::move:把对象转换成“允许被移动”的表达式
移动构造函数:真正负责转移资源

Writing the following sentence alone usually has no practical effect:

std::move(buf2);

Because the returned rvalue reference is not used for construction, assignment, or function argument passing, the move constructor will not be called.

7.2.1.3.Why is copy construction a “deep copy”?

The following statement will call the copy constructor:

Buffer buf2 = buf1;

The corresponding constructor is:

Buffer(const Buffer& other)
    : data(new int[other.size]), size(other.size)
{
    std::copy(other.data, other.data + size, data);
}

It does two things:

  1. Allocate a separate block of array memory for buf2;
  2. Copy each element in the buf1 array to the new memory.

After the copy is complete, both hold the same content, but they manage different memory:

buf1.data ──> [buf1 自己的 1000 个 int]

buf2.data ──> [buf2 自己的 1000 个 int]

Therefore, destroying or modifying one of the objects will not directly affect the other object; this is deep copy.

7.2.1.4.Why does move construction need to clear the original object?

The move constructor does not reallocate the array, nor does it copy 1000 elements; instead, it directly takes over the pointer:

data(other.data), size(other.size)

After takeover, the following must be executed:

other.data = nullptr;
other.size = 0;

Otherwise, the new object and the original object will both hold the same pointer:

buf2.data ─┐
           ├──> [同一块数组内存]
buf3.data ─┘

Both objects will execute upon destruction:

delete[] data;

This may cause the same block of memory to be freed twice, resulting in undefined behavior.

After setting the original object's pointer to nullptr, the ownership relationship becomes:

buf2.data = nullptr

buf3.data ──> [原来由 buf2 管理的数组内存]

The following operations are safe:

delete[] nullptr;

so the moved buf2 can still be destructed normally. In this custom implementation, we also explicitly set its size to 0.

7.2.1.5.Why is Buffer b = a; equivalent to Buffer b(a);

Both of the following styles are creating a new object b:

Buffer b = a;  // 拷贝初始化
Buffer b(a);   // 直接初始化

Although an equals sign appears in the first form, the = here is not an assignment operator, because b does not yet exist before this statement is executed; it is in the process of being created.

Both forms look for a constructor that can construct a using b. Since the type of a is Buffer , they both match:

Buffer(const Buffer& other);

That is, the copy constructor.

They can be understood as:

Buffer b = a;  // 创建 b,并用 a 初始化
Buffer b(a);   // 创建 b,并用 a 初始化

For this example, the two notations have the same effect.

7.2.1.6.Judgment method for construction and assignment

Whether a constructor or an assignment operator is called depends not on whether there is an equals sign, but on whether the object on the left is being created.

Buffer a(1000);

Buffer b = a;  // b 正在创建:调用拷贝构造函数
Buffer c(a);   // c 正在创建:调用拷贝构造函数

Buffer d = std::move(a);  // d 正在创建:调用移动构造函数
Buffer e(std::move(b));   // e 正在创建:调用移动构造函数

If the object already exists, then using the equals sign is an assignment:

Buffer x(100);
Buffer y(200);

x = y;             // x 已存在:调用拷贝赋值运算符
x = std::move(y);  // x 已存在:调用移动赋值运算符

Can be remembered in one sentence:

The constructor is called when an object is "being born"; the assignment operator is called when the object already exists and only its contents are being replaced.

The correspondence is as follows:

Syntax Object state Called function
Buffer b = a; b Creating copy constructor
Buffer b(a); b Creating copy constructor
Buffer b = std::move(a); b Creating move constructor
Buffer b(std::move(a)); b Creating move constructor
b = a; b Already exists copy assignment operator
b = std::move(a); b Already exists move assignment operator
7.2.1.7.Copy initialization and direct initialization are not always the same

From a grammatical classification perspective:

Buffer b = a;  // copy-initialization,拷贝初始化
Buffer b(a);   // direct-initialization,直接初始化

In the current Buffer example, both invoke the normal copy constructor, so the effect is the same. However, when faced with a conversion constructor with explicit, a difference arises between the two.

For example:

class Number
{
public:
    explicit Number(int value)
    {
    }
};

Direct initialization can explicitly call this constructor:

Number n1(10);  // 正确

Copy initialization does not allow implicit use of the explicit constructor:

Number n2 = 10;  // 错误

Therefore, a more accurate statement is:

For the copy of objects of the same type in this example, Buffer b = a; and Buffer b(a); both call the copy constructor; however, "copy initialization" and "direct initialization" are not completely equivalent in all scenarios.

Note: The current Buffer class manually manages dynamic memory, but only implements the destructor, copy constructor, and move constructor. To also safely support b = a and b = std::move(a) copy assignment operator and move assignment operator should also be implemented, which is commonly called the Rule of Five.

7.3.Example 3: Building on Example 2, move semantics make returning large objects from functions efficient

#include <iostream>
#include <vector>
#include <string>

// 返回大 vector(C++11 起自动启用移动语义,不需要手动 std::move)
std::vector<int> make_data(int n)
{
    std::vector<int> v(n);
    for (int i = 0; i < n; ++i)
    {
        v[i] = i * 10;
    }
    return v;  // ✅ 编译器自动移动(或 RVO 优化),不拷贝
}

int main()
{
    auto data = make_data(5);
    std::cout << "data: ";
    for (int n : data)
    {
        std::cout << n << " ";
    }
    std::cout << "\n";
    std::cout << "size = " << data.size() << "\n";

    // ⚠️ 错误做法:不要对返回值用 std::move!
    // auto data2 = std::move(make_data(5));  // ❌ 不要这样写!破坏 RVO 优化

    // ✅ 正确做法:赋值给已有变量时用 = std::move(source)
    std::vector<int> old = {100, 200};
    std::vector<int> fresh = std::move(old);  // 把 old 的内容移给 fresh
    std::cout << "old size = " << old.size() << "\n";      // 0
    std::cout << "fresh size = " << fresh.size() << "\n";  // 2

    return 0;
}

Running result:

data: 0 10 20 30 40 
size = 5
old size = 0
fresh size = 2

8.Key syntax explanation in the example

示例 讲了什么 新出现的语法 为什么这样写 注意事项
示例 1 拷贝 vs 移动对比 std::move()、移动后 size=0 move 后源对象被"掏空",变成有效但未指定状态 move 后不要再使用源对象(除非重新赋值)
示例 2 自定义移动构造 Buffer(Buffer&&)、noexcept、other.data=nullptr 移动构造直接偷指针,不分配新内存 移动后必须把 other.data 置 null,否则重复 delete
示例 3 返回值和赋值优化 RVO、return v 不加 move 编译器会优化返回值为移动/RVO,加了 move 反而阻止 RVO 局部变量 return 时不要加 std::move

9.Why must the move constructor be declared noexcept?

std::vectorDuring expansion, the old elements must be moved to new memory. If the move constructor of the element type may throw exceptions,vector to ensure exception safety, it may be preferable to use the copy constructor instead. In other words, even if you have written a move constructor, if you have not written noexcept, the container may not necessarily use it during expansion.

class Buffer
{
public:
    Buffer(Buffer&& other) noexcept
    {
        // 偷资源,不抛异常
    }
};

This difference is not obvious when moving a single object; only when vector<Buffer> large numbers push_back trigger reallocation does noexcept whether the container chooses to move or copy come into play.

10.When should std::move be used, and when should it not be used?

Scenario Whether to use std::move Reason
Transfer the resources of an existing object to another object ✅ Explicitly transfer ownership or contents
Pass unique_ptr to a function that takes ownership ✅ unique_ptr cannot be copied, only moved
return local variable ❌ Let the compiler perform RVO / NRVO
For a const object ❌ A const object cannot be truly moved, and is usually turned into a copy
After move, still want to continue reading the contents of the original object ❌ moved-from The object is only guaranteed to be destructible and reassignable

11.Common Errors

Error 1: Continuing to use the source object after move

std::vector<int> a = {1, 2, 3};
std::vector<int> b = std::move(a);
std::cout << a[0];  // ❌ a 已经被"掏空",行为未定义

Correct approach: after move, do not use the source object again, or check whether it is null first.

Mistake 2: Using move on const objects

const std::vector<int> a = {1, 2, 3};
std::vector<int> b = std::move(a);  // ❌ 实际执行的是拷贝!const 对象不能移动

Correct usage: move should not be used on const objects.

Mistake 3: Adding std::move to the return value

std::vector<int> func()
{
    std::vector<int> v(1000);
    return std::move(v);  // ❌ 阻止了 RVO 优化!
}

Correct approach: directly return v;, the compiler will optimize automatically.

Error 4: The move constructor is not marked noexcept

MyClass(MyClass&& other) { ... }  // 缺少 noexcept

Correct approach:MyClass(MyClass&& other) noexcept { ... } — Not marking it noexcept causes containers such as vector to fall back to copying during reallocation.

12.Usage Recommendations

  1. Do not add std::move on return: let the compiler optimize automatically (RVO/NRVO).
  2. Use when assigning to an existing object std::move(source): use target = std::move(source) to avoid copying.
  3. Do not use the source object again after move(unless reassigned or reset).
  4. Move constructors must be marked noexcept: otherwise STL containers will not call it during reallocation.
  5. std::move does not move anything: it is just a type cast that turns an lvalue into an rvalue reference. The actual move happens in the move constructor or move assignment operator.

13.Summary

  • An lvalue has a name and is addressable; an rvalue is temporary (a literal, a temporary object).
  • An rvalue reference T&& binds to an rvalue.
  • Move semantics "steal" the rvalue's resources, avoiding a deep copy.
  • std::move(x) Converts an lvalue to an rvalue, telling the compiler that "x can be stolen".
  • When returning a local variable, do not add std::move (let the compiler optimize it itself).
  • A move constructor must be marked with noexcept.

13.1.Engineering Expansion

In ROS2, when publishing messages and transmitting large amounts of sensor data (such as point clouds and images), move semantics can avoid copying large data. In Boost.Asio, callback parameters for asynchronous operations make extensive use of move semantics to efficiently pass data buffers.