Rust如何提供移动语义?

k4aesqcs  于 2022-11-12  发布在  其他
关注(0)|答案(6)|浏览(218)

Rust language website声称move语义是该语言的特性之一,但我看不出move语义是如何在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根本没有构造函数,更不用说移动构造函数了。
  • 不支持右值引用。
  • 无法使用右值参数创建函数重载。

Rust如何提供移动语义?

cs7cruho

cs7cruho1#

我认为这是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"
}
qcbq4gxm

qcbq4gxm2#

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的方法被称为“破坏性移动”。有人建议在C中添加“移动析构函数”或类似的东西,这样它就可以具有相同的语义。但是在C++中实现的移动语义并不这样做。旧的对象被留下,它的析构函数仍然被调用。因此,你需要一个移动构造函数来处理移动操作所需的自定义逻辑。2移动只是一个特殊的构造函数/赋值运算符,它应该以某种方式来表现。
因此默认情况下,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
但在我看来,这种取舍是值得的。

ycl3bljg

ycl3bljg3#

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

*所有类型均可移动。
***默认情况下,在整个语言中,将值发送到某个地方是一种移动。**对于非Copy类型,如Vec,以下是Rust中的所有移动:按值传递参数、返回值、赋值、按值模式匹配。

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

***Rust知道移动的值不能被使用。**如果你有一个值x: String,并执行channel.send(x),将该值发送到另一个线程,编译器知道x已经被移动。在移动后试图使用它是一个编译时错误,“使用移动的值”。如果有人引用了一个值(一个悬空指针),你就不能移动它。
***Rust知道不要在移动的值上调用析构函数。**移动一个值会转移所有权,包括清理的责任。类型不必能够表示一个特殊的“值已移动”状态。
*移动很便宜而且性能是可预测的。它基本上是memcpy。返回一个巨大的Vec总是很快的--你只需要复制三个单词。
***Rust标准库在任何地方都使用并支持移动。**我已经提到了通道,它使用移动语义在线程之间安全地传输值的所有权。其他优点:Rust中所有类型都支持无拷贝std::mem::swap(); IntoFrom标准转换特性是按值的; Vec和其他集合具有.drain().into_iter()方法,因此您可以粉碎一个数据结构,将其中的所有值移出,并使用这些值构建一个新的数据结构。

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

ezykj2lf

ezykj2lf4#

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

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

然后让我们将s赋值给t

let t = s;

这就是发生的事情:

let t = s将向量的三个报头字段从s移动到t;现在,t是向量的所有者,向量的元素保持在原来的位置,字符串也没有任何变化,每个值仍然有一个所有者。
现在s被释放了,如果我写这个

let u = s

我收到错误:移动值使用:s英寸
Rust将move语义应用到几乎任何值的使用上(除了Copy类型)。从一个函数返回一个值会把所有权转移到调用者。2构建一个元组会把值转移到元组中。3等等。
Reference:Programming Rust by Jim Blandy, Jason Orendorff, Leonora F. S. Tindall

ovfsdjhp

ovfsdjhp5#

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

3xiyfsfu

3xiyfsfu6#

在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

相关问题