【问题标题】:How does Rust provide move semantics?Rust 如何提供移动语义?
【发布时间】:2015-06-11 23:53:45
【问题描述】:

Rust language website 声称将语义作为语言的特征之一。但是我看不到 Rust 是如何实现移动语义的。

Rust 盒子是唯一使用移动语义的地方。

let x = Box::new(5);
let y: Box<i32> = x; // x is 'moved'

上面的Rust代码可以用C++写成

auto x = std::make_unique<int>();
auto y = std::move(x); // Note the explicit move

据我所知(如果我错了,请纠正我),

  • Rust 根本没有构造函数,更不用说移动构造函数了。
  • 不支持右值引用。
  • 无法使用右值参数创建函数重载。

Rust 如何提供移动语义?

【问题讨论】:

  • 大多数情况下,C++ 会隐式复制,Rust 会隐式移动。这不仅适用于盒子。
  • “这种语言没有任何个 C++ 必须支持移动的出色、极其复杂、容易出错的技巧!”你没有错……;-)

标签: rust move-semantics


【解决方案1】:

我认为这是来自 C++ 的一个非常常见的问题。在 C++ 中,当涉及到复制和移动时,您会明确地做所有事情。该语言是围绕复制和参考设计的。使用 C++11,“移动”东西的能力被粘在了那个系统上。另一方面,Rust 有了新的开始。


Rust 根本没有构造函数,更不用说移动构造函数了。

您不需要移动构造函数。 Rust 移动所有“没有复制构造函数”的东西,也就是“没有实现 Copy 特征”。

struct A;

fn test() {
    let a = A;
    let b = a;
    let c = a; // error, a is moved
}

Rust 的默认构造函数(按照惯例)只是一个名为 new 的关联函数:

struct A(i32);
impl A {
    fn new() -> A {
        A(5)
    }
}

更复杂的构造函数应该有更具表现力的名字。这是 C++ 中命名构造函数的习惯用法


不支持右值引用。

它一直是一个请求的功能,请参阅RFC issue 998,但很可能您要求的是不同的功能:将内容移动到函数:

struct A;

fn move_to(a: A) {
    // a is moved into here, you own it now.
}

fn test() {
    let a = A;
    move_to(a);
    let c = a; // error, a is moved
}

无法使用右值参数创建函数重载。

你可以用特质做到这一点。

trait Ref {
    fn test(&self);
}

trait Move {
    fn test(self);
}

struct A;
impl Ref for A {
    fn test(&self) {
        println!("by ref");
    }
}
impl Move for A {
    fn test(self) {
        println!("by value");
    }
}
fn main() {
    let a = A;
    (&a).test(); // prints "by ref"
    a.test(); // prints "by value"
}

【讨论】:

  • 那么你真的错过了 C++ 的一个特性还是 Rust 只是做不同的事情?
  • 在 rust 中,创建引用不是显式移动,而是显式创建引用:let x = &amp;a; 创建一个名为 x 的 (const) 引用到 a。此外,在优化方面您应该信任编译器,以防您担心隐式移动会造成性能损失。由于编译器内置了移动语义,编译器可以进行很多优化。
  • 另外,rust 仍然有隐式副本。您只需要为您的类型实现 Copy 特征,然后从现在开始复制它。对于 POD,您甚至可以告诉编译器自动为您生成 Copy 特征实现。
  • @TheParamagneticCroissant:Rust 不需要“删除”先前位置的移动构造函数,因为一旦您移出某个位置,就会设置一个标志,该对象不应调用 Drop::drop。将来,改进的分析实际上将确保我们不再需要这样的标志。我不确定其中实施了多少。
  • 所以一旦实现Copy,就不能强制移动对象/类/whatchamacallit-in-rust?
【解决方案2】:

Rust 的移动和复制语义与 C++ 非常不同。我将采用与现有答案不同的方法来解释它们。


在 C++ 中,由于自定义的复制构造函数,复制是一种可以任意复杂的操作。 Rust 不想要简单赋值或参数传递的自定义语义,因此采用了不同的方法。

首先,在 Rust 中传递的赋值或参数始终只是一个简单的内存副本。

let foo = bar; // copies the bytes of bar to the location of foo (might be elided)

function(foo); // copies the bytes of foo to the parameter location (might be elided)

但是如果对象控制了一些资源呢?假设我们正在处理一个简单的智能指针Box

let b1 = Box::new(42);
let b2 = b1;

此时,如果只复制字节,是否会为每个对象调用析构函数(Rust 中的drop),从而释放相同的指针两次并导致未定义的行为?

答案是 Rust 默认会移动。这意味着它将字节复制到新位置,然后旧对象就消失了。在上面第二行之后访问b1 是编译错误。并且不需要析构函数。该值已移至b2,而b1 可能不再存在。

这就是 Rust 中移动语义的工作方式。字节被复制过来,旧对象消失了。

在一些关于 C++ 移动语义的讨论中,Rust 的方式被称为“破坏性移动”。已经有人提议添加“移动析构函数”或类似于 C++ 的东西,以便它可以具有相同的语义。但是移动语义在 C++ 中实现时不会这样做。旧的对象被留下了,它的析构函数仍然被调用。因此,您需要一个移动构造函数来处理移动操作所需的自定义逻辑。移动只是一个专门的构造函数/赋值运算符,预期会以某种方式运行。


所以默认情况下,Rust 的赋值会移动对象,使旧位置无效。但是许多类型(整数、浮点、共享引用)具有复制字节的语义是创建真实副本的一种完全有效的方式,无需忽略旧对象。这样的类型应该实现Copy trait,它可以由编译器自动派生。

#[derive(Copy)]
struct JustTwoInts {
  one: i32,
  two: i32,
}

这向编译器表明赋值和参数传递不会使旧对象无效:

let j1 = JustTwoInts { one: 1, two: 2 };
let j2 = j1;
println!("Still allowed: {}", j1.one);

请注意,琐碎的复制和销毁的需要是相互排斥的; Copy 的类型 不能 也可以是 Drop


现在,当您想要复制仅复制字节还不够的内容时,例如一个向量?这没有语言功能;从技术上讲,该类型只需要一个返回以正确方式创建的新对象的函数。但按照惯例,这是通过实现 Clone 特征及其 clone 函数来实现的。事实上,编译器也支持Clone 的自动派生,它只是克隆每个字段。

#[Derive(Clone)]
struct JustTwoVecs {
  one: Vec<i32>,
  two: Vec<i32>,
}

let j1 = JustTwoVecs { one: vec![1], two: vec![2, 2] };
let j2 = j1.clone();

无论何时你派生Copy,你也应该派生Clone,因为像Vec这样的容器在克隆它们自己时会在内部使用它。

#[derive(Copy, Clone)]
struct JustTwoInts { /* as before */ }

现在,这有什么缺点吗?是的,实际上有一个相当大的缺点:因为将对象移动到另一个内存位置只是通过复制字节来完成,没有自定义逻辑,cannot have references into itself 类型。事实上,Rust 的生命周期系统使得安全地构造此类类型变得不可能。

但在我看来,这种权衡是值得的。

【讨论】:

  • 移动位于堆栈上的内存也有意义吗?示例:rust let i: i32 = 12; let obj = MyStruct(i); 为堆栈上的两个 i32 变量分配空间 - 表示 8 个字节。但实际上在第二行移动后只需要一个。
  • @Matthias 编译器可能会决定进行此优化;但它可能在 LLVM 级别,在 Rust 的语义之外。
  • @SebastianRedl 所以在 Rust 中,movecopy 都是 memcpy,而 move 不允许使用原始文件。 Clone trait 将智能的深拷贝委托给类型作者。我的理解正确吗?感谢您的回答,您的解释了幕后发生的事情!
  • @legends2k 是的,深拷贝必须由Clone 实施。移动是 memcpy。
  • 谢谢! Copy trait 同意我的总结;只是把它放在这里给未来的读者。
【解决方案3】:

Rust 支持具有以下特性的移动语义:

  • 所有类型都是可移动的。

  • 默认情况下,在整个语言中向某处发送值是一种移动。对于非Copy 类型,如Vec,以下都是 Rust 中的移动:通过值,返回值,赋值,按值模式匹配。

    Rust 中没有std::move,因为它是默认值。你真的一直在使用动作。

  • Rust 知道不能使用移动的值。 如果你有一个值 x: String 并执行 channel.send(x),将值发送到另一个线程,编译器知道 @987654326 @ 已被移动。在移动后尝试使用它是一个编译时错误,“使用移动的值”。如果有人对它有引用(悬空指针),你就不能移动一个值。

  • Rust 知道不对移动的值调用析构函数。移动值会转移所有权,包括清理的责任。类型不必能够表示特殊的“值已移动”状态。

  • 价格低廉,性能可预测。它基本上是memcpy。返回一个巨大的Vec 总是很快的——你只是复制三个单词。

  • Rust 标准库在任何地方都使用并支持移动。我已经提到了通道,它使用移动语义来安全地跨线程传输值的所有权。其他不错的地方:所有类型都支持 Rust 中的无复制 std::mem::swap()IntoFrom 标准转换特征是按值的; Vec 和其他集合具有 .drain().into_iter() 方法,因此您可以粉碎一个数据结构,将所有值移出,然后使用这些值构建一个新结构。

Rust 没有移动引用,但移动是 Rust 中一个强大且核心的概念,提供许多与 C++ 相同的性能优势,以及一些其他优势。

【讨论】:

    【解决方案4】:

    我想补充一点,不需要移动到memcpy。如果堆栈上的对象足够大,Rust 的编译器可能会选择传递对象的指针。

    【讨论】:

      【解决方案5】:

      在 C++ 中,类和结构的默认分配是浅拷贝。值被复制,但不是指针引用的数据。所以修改一个实例会改变所有副本的引用数据。在另一种情况下,值(例如用于管理)保持不变,可能会呈现不一致的状态。移动语义避免了这种情况。具有移动语义的内存管理容器的 C++ 实现示例:

      template <typename T>
      class object
      {
          T *p;
      public:
          object()
          {
              p=new T;
          }
          ~object()
          {
              if (p != (T *)0) delete p;
          }
          template <typename V> //type V is used to allow for conversions between reference and value
          object(object<V> &v)      //copy constructor with move semantic
          {
              p = v.p;      //move ownership
              v.p = (T *)0; //make sure it does not get deleted
          }
          object &operator=(object<T> &v) //move assignment
          {
              delete p;
              p = v.p;
              v.p = (T *)0;
              return *this;
          }
          T &operator*() { return *p; } //reference to object  *d
          T *operator->() { return p; } //pointer to object data  d->
      };
      

      这样的对象会自动被垃圾回收,并且可以从函数返回给调用程序。它非常高效,并且与 Rust 一样:

      object<somestruct> somefn() //function returning an object
      {
         object<somestruct> a;
         auto b=a;  //move semantic; b becomes invalid
         return b;  //this moves the object to the caller
      }
      
      auto c=somefn();
      
      //now c owns the data; memory is freed after leaving the scope
      

      【讨论】:

      • 这似乎没有回答 OP 提出的问题:Rust 如何提供移动语义?。这个答案似乎是在讨论 C++ 如何做类似的事情。
      猜你喜欢
      • 2016-07-11
      • 1970-01-01
      • 2021-11-19
      • 2011-06-29
      • 2012-04-14
      • 1970-01-01
      • 2020-04-28
      相关资源
      最近更新 更多