C++/std::move

67 阅读5分钟

std::move做了什么?它保证会“移动”吗?

左值和右值

通俗来说:

  1. **左值:**可以取地址、有名字、能长期存在的
  2. **右值:**取不了地址、没名字、即将消失的临时结果
左值 (lvalue):有固定地址的“信箱”

代表一个在内存中有确定位置的对象或变量

  1. 有持久的身份:在表达式结束后,它依然存在
  2. 可以被取地址:你可以对一个左值使用 & 运算符
  3. 可以被赋值:它能出现在赋值符号 (=) 的左边

常见例子:

  1. 所有有名字的变量:int x = 10; (这里的 x 就是一个左值)
  2. 返回左值引用的函数调用:std::getline(std::cin, str) 中的 std::cin
int x;      // x 是一个左值
x = 100;    // 正确:左值可以被赋值
int* p = &x; // 正确:左值可以被取地址
右值 (rvalue):即将消失的“信件”

右值(rvalue)的 “r” 可以理解为 read(读取)。它通常指一个临时的计算结果,没有固定的内存地址,在一个表达式结束之后就会被销毁

  1. 是临时的:表达式一结束,它就“烟消云散”
  2. 不能被取地址:对一个右值使用 & 运算符通常是错误的
  3. 不能被赋值:它不能出现在赋值符号 (=) 的左边

eg:

  1. 字面量:10true'A'
  2. 算术表达式的结果:x + 5a * b
  3. 按值返回的函数调用:int get_number() { return 42; },这里的 get_number() 的返回值就是一个右值

对左值(信箱):你只能拷贝 (copy) 里面的东西,因为原来的主人还需要它

对右值(信件):因为这封信读完就要扔掉,所以你可以直接**“窃取” (move)** 它的内容,而不用花力气去复制

C++11引入了右值引用 (&&),它允许“捕获”这些即将被销毁的右值,并“窃取”它们的内部资源(如动态分配的内存),从而避免了昂贵的深拷贝操作


std::move

std::move的本质:一个“可以被拿走”的信号

一个装满贵重物品的箱子 (Box A),现在想把这些物品放进一个新箱子 (Box B)

  1. 拷贝 (Copy):把Box A里的物品一件一件地复制出来,再把复制品放进Box B。这个过程很慢,而且消耗新材料。Box A里的物品原封不动
  2. 移动 (Move):直接把Box A里的所有物品整体搬到Box B里。这个过程非常快。Box A虽然还在,但它里面已经空了

std::move就好比在Box A上贴一个标签,上面写着:“可以拿走,我不用了”。它没有搬任何东西,只是发出了一个信号

看到这个信号的“搬家工人”(即移动构造函数或移动赋值运算符)就知道可以安全地“搬空”这个箱子,而不用担心原来的主人还需要它

这个“类型转换”等价于:

static_cast<T&&>(lvalue)

真正的移动是如何发生的?

class ResourceHolder {
public:
    std::string name;
    std::vector<int> data;

    // 默认构造函数
    ResourceHolder(const std::string& n) : name(n) {
        data.resize(1000); // 模拟一个很大的资源
        std::cout << "Constructing " << name << " (regular).\n";
    }

    // 拷贝构造函数 (深拷贝)
    ResourceHolder(const ResourceHolder& other) 
        : name(other.name + " (copy)"), data(other.data) {
        std::cout << "Constructing " << name << " using COPY.\n";
    }

    // 移动构造函数 (浅拷贝/资源窃取)
    ResourceHolder(ResourceHolder&& other) noexcept
        : name(std::move(other.name)), data(std::move(other.data)) {
        name += " (moved)";
        std::cout << "Constructing " << name << " using MOVE.\n";
    }
};

int main() {
    ResourceHolder holder1("Holder1");

    std::cout << "\n--- Trying to copy ---\n";
    ResourceHolder holder2 = holder1; // 调用拷贝构造函数

    std::cout << "\n--- Trying to move ---\n";
    // std::move将holder1转换为右值引用,因此匹配并调用移动构造函数
    ResourceHolder holder3 = std::move(holder1);

    std::cout << "\nAfter move, holder1's name is: '" << holder1.name << "'\n";
    std::cout << "After move, holder1's data size is: " << holder1.data.size() << "\n";
    return 0;
}
-------------
/*
Constructing Holder1 (regular).

--- Trying to copy ---
Constructing Holder1 (copy) using COPY.

--- Trying to move ---
Constructing Holder1 (moved) using MOVE.

After move, holder1's name is: ''
After move, holder1's data size is: 0
*/
  1. ResourceHolder holder2 = holder1;holder1是一个左值,因此调用拷贝构造函数holder2得到了holder1数据的一份完整拷贝

  2. ResourceHolder holder3 = std::move(holder1);std::move(holder1)holder1转换成右值引用,因此移动构造函数被调用。holder3没有复制数据,而是直接“窃取”了

    holder1namedatastd::vectorstd::string内部也实现了移动语义)

  3. 移动后,原来的holder1对象进入了一个有效但未指定的状态。它的资源已经被“搬走”,不能再对其值做任何假设,但它本身仍然是一个可以被安全销毁的对象


总结:std::move的关键点

  1. 不移动,只转换类型std::move是一个信号,告诉编译器可以进行移动优化
  2. 不保证移动:如果一个类没有定义移动构造函数/移动赋值运算符,那么即使使用了std::move,编译器仍然会退回到拷贝操作
  3. 何时使用:确定一个对象(特别是拥有堆内存等昂贵资源的对象)在后续代码中不再需要其当前值时,就应该使用std::move将其资源转移给新对象,以避免不必要的深拷贝
  4. 不要对一个之后还想正常使用的对象调用std::move。一旦其资源被移走,它的状态就不再可靠