std::move 使用

date
May 3, 2022
Last edited time
Mar 27, 2023 08:52 AM
status
Published
slug
std::move使用
tags
Programing
summary
type
Post
origin
Field
Plat

右值

右值的更详细的分可以分为纯右值和亡值,更具体的来说 std::move 返回的右值是其中的亡值,想看具体解释去看 cppreference 值类型的讲解就好了。
但用非常简短的话来表述右值是什么的话其实很简单,就一句话,计算过程中的临时值。比如你在 if 表达式的括号里进行了布尔运算,这个运算结果就是一个右值。或者你让整数 a 等于数字 1,那么写在等号右边的 1 就是一个右值。
对象都有一个称之为生命周期的属性,表述了一个对象从实例化到消失之间的时间。对于左值而言,我们可以认为每一个大括号都表述了一个生命周期。同时,大的区间包含了小的区间。因此,比如你在 main 函数内声明的变量都进一步在函数体内的各种循环、条件语句的区间内存在,但是离开了函数就不再有效。右值与之不同,它在表达式中产生,在表达式的运算结束后就立刻消失,比如上文说的让一个整数等于一个字面量 1。这种现象体现了右值 “阅后即焚” 的特性,同时又隐喻了一个右值潜在的性质:只阅读一次,因此用户并不清楚右值后来怎样了,也不在乎它是怎么消失的。也即是说:如何条件合适,我们可以善用生命周期短暂的右值作为边角料达成一些特殊的操作。
如果代码中涉及大量的资源分配过程,在数据的构造和析构指令就会产生大量的系统调用,从而导致性能严重下降。在没有移动语义之前,默认的拷贝构造 / 赋值的方式是:
  1. 构造 / 赋值操作符获得一个参数(暂时只考虑右值的情况,此时右值生命周期开始)
  1. 若是构造就为新对象分配内存,若是赋值就检测内存大小是否足够后再考虑分配(malloc 或者 new)
  1. 从参数处复制数据(可能是一个线性的过程,也可能存在迭代,或是包含更复杂的算法)
  1. 最后析构临时对象(右值生命周期结束,如果类型包含堆上的数据,就有 free 或 delete 调用)
可以看到这个过程存在大量潜在的系统调用,频繁调用这些指令必然会明显影响性能。那么鉴于右值的上述特性,那么如果我们可以利用右值毁灭前的数据,不是将其销毁,而是将资源的所有权转移给目标,就可以通过减少系统调用而提高性能。在你的代码中,使用整数来考虑右值的实际意义多少有点杀鸡用牛刀,如果考虑一个稍微复杂点情况就能明白了
对于这样的情况,括号中的临时对象自己就调用了一轮的 new 和 delete。而这个申请的过程其实从最终结果看来可能并相当冗余。移动构造提供了一个解决办法:
这么做之后,作为左值的对象就获得了临时对象的资源的 “控制权”,同时临时对象也可以安全析构而不会导致数据失效。同理的还有移动赋值,它是对操作符 = 的重载,接受一个自身类的实例的右值。
绕了一大圈终于该说 std::move 的问题了
std::move 的语义很简单,你也说了,“将一个左值转换为一个右值”。一旦一个左值被转换为右值,那么这个右值就可以用来代入接受右值的函数 / 操作符中。这里牵扯一点点函数重载的问题:你可以重载构造函数或是赋值操作符:
根据你代入函数的实参不同,类会调用不同的函数。如果代入左值,调用的就是拷贝,如果是右值,调用的就是移动。在 C++ 的语法中,有很多原生的右值,比如刚才所说的计算产生的临时值,比如函数的返回值,这些都是天然的右值,所以一旦用这些值代入函数,就会调用形参为右值的重载版本。
但这里有另一种情况,一旦某些由代码编写者进行的计算生成了一个值并被保存在一个左值中,而经过了一些处理之后,或许这个左值已经不再被需要。我们在转移这个左值的数据的过程中如果直接通过某些函数进行转移就必然调用使用左值的作为形参的版本。如果我们真的确信:这个左值已经完全不会被需要了,在这个左值的生命周期结束前,再也不会被使用到,那么 C++ 允许我们使用一种转换,将左值转化为右值。在此之后,我们就可以调用右值作为形参的重载函数。这样的函数就如上面的移动构造一样,实际上等于对右值进行了肆意的破坏,同时仅保证其最低的析构安全性。在经过了这样的转化之后,右值内部到底处于什么状态是由那个重载了右值作为形参的函数和这个对象本身的类的实现决定的。标准要求我们不要对其做任何期待,因此使用这样人工转化的右值是不安全。
总结
std::move 进行了一种人工的转化,在转化之后,其返回了一个右值。通过使用这个右值,我们可以调用任何重载了使用右值作为形参的版本的函数。这样的函数一般利用了 “避免复制,而是转移资源控制权” 这一理念,从而高效的转移被分配的内存,避免了多次重分配内存带来的性能降低。其代价是,我们必须有意识地遵守一个基本规定,“右值是不可使用的”。在使用了 std::move 转化出的右值之后,对于原始的对象我们不要对其进行任何的访问、修改,静待它被析构即可。

© Lazurite 2021 - 2024