繁体   English   中英

Rust 如何提供移动语义?

[英]How does Rust provide move semantics?

Rust 语言网站声称移动语义是该语言的特征之一。 但是我看不到移动语义在 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 如何提供移动语义?

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


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

您不需要移动构造函数。 Rust 移动所有“没有复制构造函数”的东西,也就是“没有实现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++ 中的命名构造函数习语


不支持右值引用。

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

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

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

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是编译错误。 并且不会调用析构函数。 该值已移至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 trait 及其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 的生命周期系统使得安全地构造这样的类型是不可能的。

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

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++ 中相同的性能优势,以及其他一些优势。

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

这就是它在 memory 中的表示方式

在此处输入图像描述

然后让我们将 s 分配给 t

 let t = s;

这是发生了什么:

在此处输入图像描述

let t = s将向量的三个 header 字段从 s 移动到 t; 现在 t 是向量的所有者。 矢量的元素保持原样,字符串也没有任何变化。 每个值仍然只有一个所有者。

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

  let u = s

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

Rust 将移动语义应用于几乎所有值的使用(复制类型除外)。 将 arguments 传递给函数会将所有权转移到函数的参数; 从 function 返回一个值将所有权转移给调用者。 构建元组将值移动到元组中。 等等。

参考例如:Jim Blandy、Jason Orendorff、Leonora FS Tindall 的编程 Rust

原始类型不能为空并且大小固定,而非原始类型可以增长并且可以为空。 由于基本类型不能为空且大小固定,因此分配 memory 来存储它们并处理它们相对容易。 然而,非基元的处理涉及计算随着它们的增长将占用多少 memory 以及其他昂贵的操作。Wwith primitives rust 将制作副本,非原始 rust 执行移动

fn main(){
    // this variable is stored in stack. primitive types are fixed size, we can store them on stack
    let x:i32=10;
    // s1 is stored in heap. os will assign memory for this. pointer of this memory will be stored inside stack. 
    // s1 is the owner of memory space in heap which stores "my name"
    // if we dont clear this memory, os will have no access to this memory. rust uses ownership to free the memory
    let s1=String::from("my name");
    // s1 will be cleared from the stack, s2 will be added to the stack poniting the same heap memory location
    // making new copy of this string will create extra overhead, so we MOVED the ownership of s1 into s2
    let s2=s1;
    // s3 is the pointer to s2 which points to heap memory. we Borrowed the ownership
    // Borrowing is similar borrowing in real life, you borrow a car from your friend, but its ownership does not change
    let s3=&s2;
    // this is creating new "my name" in heap and s4 stored as the pointer of this memory location on the heap
    let s4=s2.clone()
}

当我们将原始或非原始类型 arguments 传递给 function 时,同样的原则适用:

fn main(){
    // since this is primitive stack_function will make copy of it so this will remain unchanged
    let stack_num=50;
    let mut heap_vec=vec![2,3,4];
    // when we pass a stack variable to a function, function will make a copy of that and will use the copy. "move" does not occur here
    stack_var_fn(stack_num);
    println!("The stack_num inside the main fn did not change:{}",stack_num);
    // the owner of heap_vec moved here and when function gets executed, it goes out of scope so the variable will be dropped
    // we can pass a reference to reach the value in heap. so we use the pointer of heap_vec
    // we use "&"" operator to indicate that we are passing a reference
    heap_var_fn(&heap_vec);
    println!("the heap_vec inside main is:{:?}",heap_vec);
}
// this fn that we pass an argument stored in stack
fn stack_var_fn(mut var:i32){
    // we are changing the arguments value
    var=56;
    println!("Var inside stack_var_fn is :{}",var);
}
// this fn that we pass an arg that stored in heap
fn heap_var_fn(var:&Vec<i32>){
    println!("Var:{:?}",var);
}

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM