第 18.10 節

Rvalue references and move semantics

0瀏覽次數0訪問次數--跳出率--平均停留

What problem does this section solve?

Consider such a scenario:

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

In C++98, returning large objects from functions caused deep copies, resulting in poor performance. C++11 introduced move semantics, which changes "copying someone else's data" to "stealing data from another object," significantly improving performance.

What is this feature?

  • lvalue: A named object that can have its address taken. For example, variables x, arr[0].
  • Right-value: A temporary, soon-to-be-destroyed object. Examples include 42, x + y, and std::move(x).
  • Rvalue Reference T&&: A reference bound to an rvalue.
  • Move semantics: Avoiding unnecessary deep copies by "stealing" a right value's resources instead of copying.
  • std::move: Converts the left value to an rvalue reference, telling the compiler, "I won't use this object anymore; you can steal its resources."

C++ standard version

C++11

Required header files

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

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 变成空

Left Value vs Right Value Cheat Sheet

Expressionlvalue/rvalueExplanation
int x = 5;x is an lvalue.Has a name and a memory address
42rvalueliteral
x + yrvalueProvisional results
f() returns non-referencervaluetemporary objects
f() returns a referencelvalueReference is an alias.
std::move(x)xvalue (expiring value)cast to an rvalue reference

Example code

Example 1: Copy vs. Move — Why We Need Move Semantics

#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;
}

Results

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

Example 2: Customizing the Move Constructor of a Class Based on Example 1

#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;
}

Results

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

Example 3: Building on Example 2, move semantics make function returns of large objects 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;
}

Results

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

runtime results

See the "running results" for each example above.

Key syntax explanation in the example

|Here is the translation of the provided Simplified Chinese Markdown fragment into natural American English, following all specified rules.


ExampleDiscusses whatNewly emerged syntaxWhy write it this wayPrecautions
Example 1Copy vs Move Comparisonstd::move(), post-movement size=0After a move, the source object is "hollowed out," becoming valid but in an unspecified state.After a move operation, do not use the source object again unless it is reassigned.
Example 2custom move constructorBuffer(Buffer&&)noexceptother.data=nullptrMove constructors directly transfer the pointer without allocating new memory.After moving, you must set other.data to null, otherwise duplicate deletions will occur.
Example 3return value and assignment optimizationRVO, return v without adding moveThe compiler will optimize return values through move/RVO, and adding a move actually prevents RVO.Avoid using std::move when returning local variables

Why should the move constructor be declared with noexcept?

Primarily to prevent the standard library containers (like std::vector) from silently falling back to copying when moving objects during operations such as resizing, which would otherwise lead to unexpected performance degradation and potential issues with non-copyable types.

When expanding capacity, std::vector must move old elements to new memory. If the move constructor for an element type might throw an exception, vector, for exception safety, the container may prefer to use the copy constructor instead. In other words: even if you've written a move constructor, if you haven't written noexcept, the container might not use it during expansion.

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

This difference isn't noticeable when moving a single object; only when vector<Buffer> triggers a large push_back expansion does noexcept potentially impact whether the container chooses to move or copy.

When to use std::move, and when not to

SceneYes, use std::move.Reason
Transfer resources from an existing object to another object.Clearly transfer ownership or content.
Pass unique_ptr to the function that receives ownership.unique_ptr cannot be copied, only moved.
return local variables让编译器执行返回值优化(RVO)或具名返回值优化(NRVO)。
For the const objectConst objects cannot be truly moved and typically become copies.
After move, still want to continue reading the original object's contentA moved-from object is only guaranteed to be destructible and reassignable.

Common Errors

Error 1: After move, continue to use the source object

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

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

Error 2: Using move on a const object

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

Correct practice: move should not be used for const objects.

Error 3: Adding std::move to Return Values

Adding std::move to a return statement is a common mistake. It prevents Return Value Optimization (RVO), which is a compiler optimization that can eliminate the copy/move entirely. In C++11 and later, you should just return the object directly; the compiler will handle efficient transfer automatically.

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

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

Error 4: Move constructor not marked noexcept

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

The correct approach: MyClass(MyClass&& other) noexcept { ... } — not marking it noexcept causes containers like vector to degrade to copying when they expand.

使用建议

  • 明确目标:在开始前确定您的具体需求,以便选择最合适的工具或教程。
  • 充分利用资源:参考官方文档、教程和博客,这些资料能帮助您快速上手并解决问题。
  • 实践应用:通过动手操作项目或编写代码来巩固学习成果,提升实际操作能力。
  • 问题解决:遇到困难时,查阅参考资料或寻求社区支持,逐步培养独立解决问题的能力。
  • 分享经验:完成项目后,可以撰写文章或博客分享心得,帮助其他学习者。

如果需要针对特定领域(如单片机、机器人或环境搭建)的进一步建议,请提供更多信息,我将为您细化内容。

  1. Do not use std::move when returning: Allow the compiler to optimize automatically (RVO/NRVO).
  2. When assigning to an existing object, use std::move(source): Use target = std::move(source) to avoid copying.
  3. Do not use the source object after moving (unless reassigning or resetting it).
  4. The move constructor must be marked as noexcept: Otherwise, STL containers won't call it during reallocation.
  5. std::move does not move anything: it is just a type conversion, converting an lvalue into an rvalue reference. The actual move happens in the move constructor or move assignment operator.

Summary

  • Lvalues have names and are addressable; rvalues are temporary (literals, temporary objects).
  • An rvalue reference T&& binds to an rvalue.
  • Move semantics "steal" resources from right-value references to avoid deep copying.
  • std::move(x) converts an lvalue to an rvalue, telling the compiler "x can be stolen."
  • When returning local variables, avoid using std::move (let the compiler optimize it itself).
  • The move constructor must be marked noexcept.

Engineering expansion

In ROS2, when publishing messages or transferring large sensor data such as point clouds or images, move semantics can help avoid copying large amounts of data. In Boost.Asio, move semantics are extensively used in the callback parameters of asynchronous operations to efficiently pass data buffers.

音乐页