Rust如何提供移动语义?

use*_*335 37 move-semantics rust

锈语言网站索赔移动语义的语言的特征之一.但我无法看到Rust中如何实现移动语义.

Rust box是唯一使用移动语义的地方.

let x = Box::new(5);
let y: Box<i32> = x; // x is 'moved'
Run Code Online (Sandbox Code Playgroud)

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

auto x = std::make_unique<int>();
auto y = std::move(x); // Note the explicit move
Run Code Online (Sandbox Code Playgroud)

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

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

Rust如何提供移动语义?

oli*_*obk 44

我认为这是来自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
}
Run Code Online (Sandbox Code Playgroud)

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

struct A(i32);
impl A {
    fn new() -> A {
        A(5)
    }
}
Run Code Online (Sandbox Code Playgroud)

更复杂的构造函数应该具有更具表现力的名称.这是C++中命名的构造函数习语


不支持右值参考.

它一直是一个请求的功能,请参阅RFC问题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
}
Run Code Online (Sandbox Code Playgroud)

无法使用rvalue参数创建函数重载.

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

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"
}
Run Code Online (Sandbox Code Playgroud)

  • @TheParamagneticCroissant:Rust不需要移动构造函数来"删除"前一个位置,因为一旦你移出某个东西,就会设置一个标志,该对象不应该调用`Drop :: drop`.在未来,改进的分析实际上将确保我们不再需要这样的旗帜.我不确定已经实施了多少. (8认同)
  • 在生锈而不是显式移动中,创建引用是显式的:`let x =&a;`创建一个名为`x`的引用(const)到`a`.此外,在涉及优化时,您应该信任编译器,以防您隐式移动会造成性能损失.由于编译器内置了移动语义,编译器可以进行很多优化. (6认同)
  • 另外,锈还有隐含的副本.你只需要为你的类型实现`Copy`特性,从现在起复制它.对于POD,您甚至可以告诉编译器自动为您生成`Copy`特征实现. (6认同)
  • 那你真的错过了C++的一个功能,或者Rust只是做了不同的事情吗? (5认同)
  • @rubenvb:正确,但您始终可以将对象包装在不实现 Copy 的包装器类型中。 (3认同)
  • @rubenvb 你不会*想要*强制移动;`Copy` 意味着按位复制足以创建一个独立的对象,而移动也只是按位复制。 (3认同)
  • 所以一旦`Copy`被实现,你就不能强制移动一个对象/类/ whatchamacallit-in-rust? (2认同)
  • @FrankHB 在 C++ 中,你是对的,移动构造函数几乎从来都不是按位复制。但是在 Rust 中,它们实际上只是按位复制,与“复制”相比,移动确实没有任何好处。 (2认同)

Jas*_*rff 12

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

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

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

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

  • Rust 知道不能使用移动的值。如果您有一个值x: String并且 dochannel.send(x)将值发送到另一个线程,编译器就会知道它x已被移动。在移动后尝试使用它是一个编译时错误,“使用移动的值”。如果有人引用某个值(悬空指针),则您无法移动该值。

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

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

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

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


Seb*_*edl 11

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)
Run Code Online (Sandbox Code Playgroud)

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

let b1 = Box::new(42);
let b2 = b1;
Run Code Online (Sandbox Code Playgroud)

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

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

这就是移动语义在Rust中的工作方式.将复制字节,旧对象消失.

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


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

#[derive(Copy)]
struct JustTwoInts {
  one: i32,
  two: i32,
}
Run Code Online (Sandbox Code Playgroud)

这表示编译器分配和参数传递不会使旧对象无效:

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

请注意,琐碎的复制和破坏的需要是相互排斥的; 一类是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();
Run Code Online (Sandbox Code Playgroud)

每当你派生出来时Copy,你也应该派生出来Clone,因为容器就像Vec在内部克隆时一样使用它.

#[derive(Copy, Clone)]
struct JustTwoInts { /* as before */ }
Run Code Online (Sandbox Code Playgroud)

现在,这有什么缺点吗?是的,事实上有一个相当大的缺点:由于移动对象到另一个存储位置只是通过复制字节完成的,没有自定义逻辑,类型不能引用到自身.实际上,Rust的生命周期系统使得无法安全地构建这样的类型.

但在我看来,权衡是值得的.