Rust如何提供移动语义?

时间:2015-04-07 11:37:58

标签: rust move-semantics

Rust language website声明将语义作为该语言的一项功能。但我看不出Rust中是如何实现移动语义的。

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

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根本没有构造函数,更不用说移动构造函数了。
  • 不支持右值参考。
  • 无法使用rvalue参数创建函数重载。

Rust如何提供移动语义?

5 个答案:

答案 0 :(得分:37)

我认为这是来自C ++时非常常见的问题。在C ++中,当涉及到复制和移动时,你正在做一切事情。该语言是围绕复制和引用而设计的。使用C ++ 11,能够移动&#34;东西粘在那个系统上。另一方面,Rust重新开始了。


  

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

您不需要移动构造函数。 Rust会移动&#34;没有复制构造函数的所有东西&#34;,a.k.a。&#34;没有实现Copy特征&#34;。

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
}

  

无法使用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"
}

答案 1 :(得分:7)

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的方式被称为“破坏性移动”#34;有人建议添加&#34;移动析构函数&#34;或类似于C ++的东西,以便它可以具有相同的语义。但是移动语义因为它们是用C ++实现的,所以不要这样做。旧对象被遗忘,其析构函数仍然被调用。因此,您需要一个移动构造函数来处理移动操作所需的自定义逻辑。移动只是一个专门的构造函数/赋值运算符,预计会以某种方式运行。

因此默认情况下,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

现在,当你想要复制一些只是复制字节的东西的时候呢?一个向量?这没有语言功能;从技术上讲,类型只需要一个返回以正确方式创建的新对象的函数。但按照惯例,这可以通过实现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的终生系统使得无法安全地构建这种类型。

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

答案 2 :(得分:0)

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

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

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

    Rust中没有std::move,因为它是默认设置。您确实一直在使用移动。

  • 必须知道不得使用已移动的值。如果您拥有值x: String并执行channel.send(x),则将值发送到另一个线程,则编译器知道x已被移动。移动后尝试使用它是一个编译时错误,“使用移动值”。而且,如果有人引用了该值(悬挂的指针),则无法移动该值。

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

  • 移动便宜,并且性能可预测。基本上是memcpy。返回巨大的Vec总是非常快-您只需复制三个词即可。

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

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

答案 3 :(得分:0)

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

答案 4 :(得分:-2)

在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