Skip to content

资源传递 移动和转发

之前说过 RAII,在构造的时候创建资源,在析构的时候释放资源,也说过左右值的事情。

这次稍微深入一点点细节,把左值引用和右值引用,以及std::movestd::forward等稍微说说。

(有一说一,如果C++也引入了所有权检查是不是会变得更清晰💦,Rust 化这块;不过有一个资源所有权的约定还是挺不错的

跨对象转移资源

假设我们需要把一个资源分发给其他对象,那么语义上有两种可能性 ——

  • 拷贝,完整的复制一份新的,和旧的无关
  • 移动,把可转移的内部资源交给新对象,源对象旧内容不再可依赖

分别对应 ——

拷贝

拷贝构造,就是把资源重新拷贝,构造新的对象,语义上 —— 复制出一份全新的资源,不影响旧资源。

优点是做了一份copy,这样新的资源不会影响旧的资源,两份都可以随意使用,所以对于源对象来说比较安全;

不过缺点也比较明显,就是完整拷贝的开销可能会比较大

移动

移动构造,把资源移动给新的 "owner",源对象旧的资源不再依赖,让新管理者管理资源。

这样优点在于规避了拷贝的开销,不过被移动对象的旧内容不能继续依赖

构造函数和赋值函数

假设手动创建一个类,那么我们需要思考构造和赋值的时候的语义

C++
class Buffer { 
  public: 
    Buffer(const Buffer& other); // 拷贝构造 
    Buffer(Buffer&& other) noexcept; // 移动构造
    Buffer& operator=(const Buffer& other); // 拷贝赋值 
    Buffer& operator=(Buffer&& other) noexcept; // 移动赋值 
};

一般来说,会选择再移动的时候把旧资源给置空;

而拷贝的时候要考虑深拷贝还是浅拷贝

  • 深拷贝:复制资源本体,新旧对象各自管理自己的资源。
  • 浅拷贝:只复制指针 / 句柄 / 引用,新旧对象指向同一份资源;如果双方都负责释放,就会重复释放。

触发条件

一般来说,在需要构造/赋值目标对象时,左值通常选择拷贝版本

C++
Buffer a(1145);
Buffer b = a;

因为 a 是左值,所以在语义上 —— "a 可能之后还有用到,我们不消耗它,而是复制一份给 b"。

而对应的,右值一般来说会触发移动

C++
Buffer b = std::move(a);

此时的语义是把“所有权”交付给 b,系统不再依赖 a,其中std::move就是把 a 从左值转化成一个右值表达式,表示 a 是可移动的

move 和 forward

std::move 之前浅说过是把表达式转化成右值表达式,它是无条件的转化,不论传入的是左值还是右值表达式都会进行转化。

std::forward 是有条件的转发 —— 传入左值引用返回左值引用;传入右值引用返回右值引用~~(听上去很没用)~~,不过实际上是 —— 根据模板参数 T 恢复原始值类别一般用于转发引用(好像又重复了一遍)

不如举例 ——

C++
void Process(int& a);                 // 接受左值引用
void Process(int&& a);                // 接受右值引用

template<typename T>
void Wrapper(T&& value)
{
	Process(std::forward<T>(value));  // 不论左右值引用,直接转发
}

int i = 1;
Wrapper(i);                           // 调用传入左值引用的方法
Wrapper(1);                           // 调用函数签名是右值引用的

因为如果正常的去 call process 方法的话,由于在函数体内的变量名会被视作左值表达式,所以无法正确调用右值引用的方法,就可以使用forward来进行转发,在STL容器设计以及日常泛型写法等都比较常用。

具体使用场景

在实际当中,一方面要考虑所有权的语义(虽然没有rust那样严格的checker,不过此处考虑的就是资源的创建和释放问题(生命周期))

另一方面也要考虑运行效率;

  • 使用移动std::move
    • 大的对象
      • vector 等容器
      • 大型的 Buffer
      • 大的封装资源
    • 明确唯一Owner的语义
      • std::unique_ptr
    • 当对象后续不再需要旧内容,并且该类型的移动有意义时
  • 不使用 move
    • 普通类型, int,float,enum
    • const 修饰的对象

以及考虑异常安全

写在最后

Effective C++ / Effective Modern C++ 写的不赖,说的很清晰

如果是我们自己管理资源,就必须明确 ——

  • 怎么释放?
  • 怎么复制?
  • 怎么移动?
  • 怎么赋值?

否则容易出现 ——

  • 内存泄漏
  • 重复释放
  • 浅拷贝
  • 悬空指针
  • 释放后继续使用

以及其他问题

Released under the MIT License.