移动语义的方方面面
主播刚刚考完计算机组成原理哈,然后浅浅荒废了几天的时间,leetcode也没刷,八股也没看,不过计算机组成原理终究是过了,姑且算对计算机有了更深入的一点点了解,过几天的信号与系统和电动力学才是折磨。
碎碎念结束。
————————————————————————————————————————————————————————————
c++11中引入了一个经常见到的函数,std::move,初次见面是在unique_ptr那一节中,后来在移动构造函数中也有见面,它的作用可以简单的理解为实现将左值转换为右值。
那么我们首先介绍一下什么是左值和右值。
左值和右值
左值,可以理解为有内存地址的值,与之相对,右值就是没有内存地址的值。
从硬件上说,左值由内存存储,右值由寄存器存储,所以右值就是马上就要消亡的值,生命周期一般只有所在的那一行代码
常见的右值形式包括:
1.int x = 5;里面的5等常量
2.调用函数的返回值
3.算术表达式或者逻辑表达式
而左值就是我们常说的变量及所有可以被左赋值的值
从而我们得到了左值引用和右值引用
1 | int a=1; |
而左值引用和右值引用又可以用同一种方式表示,即万能引用
有两种形式
1 | template<typename T> |
这里左值和右值的引用相遇,具体的结果有一个规则,即折叠引用,可以理解为两者都右值才右值,否则有一个左值,就被推导为左值
右值引用!=万能引用
右值引用是已经确定的,只能传入右值的,而万能引用由模板和auto实现,本质是不确定的
为什么要移动语义
前菜讲完,接下来是移动语义
在常见的类的赋值中,我们常常调用拷贝构造函数,而其底层是深拷贝,是需要在堆上申请空间new的,这样的空间开销是非常大的,特别是在占用空间特别大的类当中,除了这个还有两个原因
- 性能开销大: 深拷贝涉及到内存分配(可能很慢)和大量数据的复制。对于包含大量数据的对象(如
std::vector
,std::string
),这会非常耗时。 - 资源浪费: 在很多情况下,我们复制一个对象仅仅是为了临时使用,或者原对象在复制后很快就会被销毁。例如,函数返回一个大对象时,会创建一个临时对象,然后将这个临时对象拷贝到接收变量中。这个临时对象马上就没用了,但我们却花费了昂贵的代价去复制它的内容。
- 无法处理独占资源: 有些资源是不能被复制的,比如文件句柄、互斥锁(mutex)、智能指针
std::unique_ptr
等。这些资源通常是独占的。如果一个类管理着这样的独占资源,那么它的拷贝构造函数和拷贝赋值运算符就无法实现(或者被显式删除),导致这类对象无法进行正常的复制操作,限制了它们的使用场景(比如不能作为函数返回值,不能放入需要复制的标准容器)。
于是为了提高效率,处理独占资源,避免不必要的拷贝,我们采用了移动语义
举一个移动语义的例子
1 | class S |
这样的移动构造函数类似浅拷贝,参数要是右值,可以是真正的右值,也可以是move实现的左值转换成的右值
需要移动语义的典型场景:
函数返回大对象:
1
2
3
4
5
6
7std::vector<int> createLargeVector() {
std::vector<int> vec(1000000);
// 填充数据
return vec; // C++11 后,这里通常会发生移动而不是拷贝
}
std::vector<int> myVec = createLargeVector(); // 接收返回值时,使用移动构造函数在没有移动语义之前,返回 vec会创建一个临时std::vector对象,并将vec的所有元素拷贝到这个临时对象中,然后将这个临时对象拷贝到myVec中(如果 RVO/NRVO 不起作用的话)。有了移动语义,vec的资源可以直接转移给临时对象,再从临时对象转移给myVec
,避免了两次昂贵的拷贝。
将对象放入容器:
1
2
3
4
5std::vector<std::string> names;
std::string s = "very long string...";
names.push_back(s); // 拷贝
names.push_back(std::move(s)); // 移动,s 的内容被转移,s 变为空或有效但未指定状态
names.push_back("another long string..."); // 临时右值,移动当std::vector需要扩容时,它会分配新的内存并将现有元素转移到新位置。有了移动语义,这个转移过程是移动操作,而不是拷贝操作,大大提高了性能。
对象交换:
1
2
3std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = {4, 5, 6, 7, 8};
std::swap(v1, v2); // std::swap 的高效实现依赖于移动语义std::swap 的标准实现通常是:通过三次移动操作实现高效交换。
1
temp = std::move(a); a = std::move(b); b = std::move(temp);