前言

直接引用白老师的话:在开始回答问题之前,我们首先要说明:C++11 引入的移动语义,本身逻辑其实是非常的简单的,但是新增的值类别和其规则是较为繁杂的,很多人之所以不会并且对移动语义抱有错误的幻想,无非是被一些互联网的错误想法影响的。


什么是右值引用?

右值引用就是引用一个右值,和左值引用一样,算是一种引用。
C++11 引入了右值引用,并且完善了一整套规则:值类别:即左值,纯右值,亡值。这部分推荐深入学习

其实简单来说,就是将各种表达式分类,哪些是左值表达式,哪些是右值(纯右值和亡值都是右值)。右值引用只能被右值表达式初始化。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
struct X{
X() = default;
X(const X& x){
// ...todo 进行资源的复制
}
X(X&& x)noexcept {
// ...todo 进行资源的移动
}
private:
// ...todo 很多的数据成员
};

在右值引用(移动语义)出现之前,我们没办法分清楚到底需要复制?还是需要移动?在右值引用(移动语义)出现后,我们可以了。
开发者们约定移动构造这些右值引用的函数,是“转移所有权”。
你可以非常粗略的理解为,复制就是复制一份新的,但是移动呢,是把原对象的指向资源的指针,赋给新的对象成员,也就是所谓的转移所有权,通常的实现是转移指向数据的那个指针给新对象就行(当然了,这种移动是取决于你自己的移动构造移动赋值的实现的),自然没有复制开销,举一个标准库例子:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <vector>

struct Test{};

int main(){
std::vector<Test>v;
v.emplace_back(Test{});
std::cout << &v[0] << '\n';
std::vector<Test> v2{ std::move(v) };
std::cout << &v2[0] << '\n';
}

打印的地址完全一样,代表并没有拷贝元素,而只是转移所有权,转移指向实际数据的指针
一个左值表达式,要怎么匹配到右值版本的函数呢?通常我们会使用 std::move(比如刚才的示例) 这自然而然引入到了我们的下一个问题:

std::move() 到底是做什么的?

  • 让左值表达式转换为亡值表达式,用于匹配移动构造或移动赋值等函数。标志着“移动”,即转移了原对象的资源。

std::move 就是一个 static_cast 的转换,可以很粗略的这么理解吧


到底移动语义是什么?

其实前面的内容已经讲的很清楚了,移动语义很简单,方便大家区分你到底要移动还是复制罢了。

我们可以自己实现一个支持移动语义的类就懂了,参见本仓库的“所有权与移动语义”

为什么说移动更快?

这是一个非常非常经典的问题,并且错误想法众多,很多人甚至觉得:移动一个平凡类型 (如 纯数据结构体),比复制要快?

完全属于狭隘的思考,我们前面已经说的很清楚了,移动之所以更快,在于大家约定了移动构造移动赋值等函数进行的是所有权的转移,而不是资源的拷贝,你要是想让移动慢,你直接 sleep,谁能管你呢? 这些都取决于各位的实现。

或者你说你写个 int,double 什么标量类型,你说移动会更快?完全是做梦。