簡體   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