Rust 如何提供移动语义?

How does Rust provide move semantics?

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>(5);
auto y = std::move(x); // Note the explicit move

据我所知(如有错误请指正),

Rust 如何提供移动语义?

我认为这是来自 C++ 的一个非常普遍的问题。在 C++ 中,当涉及到复制和移动时,您会明确地执行所有操作。该语言是围绕复制和引用而设计的。使用 C++11,"move" 东西的能力被粘在了那个系统上。另一方面,Rust 重新开始。


Rust doesn't have constructors at all, let alone move constructors.

您不需要移动构造函数。 Rust 会移动 "does not have a copy constructor"、a.k.a 的所有内容。 "does not implement the Copy trait".

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++ 中的命名构造函数习语


No support for rvalue references.

它一直是一个请求的功能,请参阅 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
}

No way to create functions overloads with rvalue parameters.

你可以用特征来做到这一点。

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"
}

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是一个编译错误。并且不需要析构函数。该值已移至 b2b1 也可能不再存在。

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

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


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

#[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.


现在,当您想复制仅复制字节是不够的东西时怎么办?向量?没有语言功能;从技术上讲,该类型只需要一个函数 returns 一个以正确方式创建的新对象。但按照惯例,这是通过实现 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 */ }

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

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

在 C++ 中,类 和结构的默认赋值是浅拷贝。复制值,但不复制指针引用的数据。因此修改一个实例会更改所有副本的引用数据。值(f.e。用于管理)在另一个实例中保持不变,可能呈现不一致状态。移动语义避免了这种情况。具有移动语义的内存管理容器的 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

Rust 支持具有以下功能的移动语义:

  • 所有类型均可移动

  • 默认情况下,在某处发送一个值是整个语言中的一个动作。对于非Copy类型,例如Vec],以下是 Rust 中的所有动作:按值传递参数、返回值、赋值、按值进行模式匹配。

    Rust 中没有 std::move,因为它是默认设置。你真的一直在用招式

  • Rust 知道不能使用移动的值。 如果你有一个值 x: String 并执行 channel.send(x),发送值传递给另一个线程,编译器知道 x 已被移动。移动后尝试使用它是编译时错误,"use of moved value"。如果有人引用了某个值(悬空指针),则您无法移动该值。

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

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

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

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

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

let s = vec!["udon".to_string(), "ramen".to_string(), "soba".to_string()];

这是它在内存中的表示方式

那我们把s赋值给t

 let t = s;

事情是这样的:

let t = s 将向量的三个 header 字段从 s 移动到 t;现在 t 是向量的所有者。向量的元素保持不变 他们在哪里,琴弦也没有任何变化。每个值仍然只有一个所有者。

现在s被释放了,如果我这样写

  let u = s

我收到错误消息:“使用移动值:s

Rust applies move semantics to almost any use of a value (Except Copy types). Passing arguments to functions moves ownership to the function’s parameters; returning a value from a function moves ownership to the caller. Building a tuple moves the values into the tuple. And so on.