简体   繁体   English

Rust 中的移动语义是什么?

[英]What are move semantics in Rust?

In Rust, there are two possibilities to take a reference在 Rust 中,有两种可能引用

  1. Borrow , ie, take a reference but don't allow mutating the reference destination. Borrow ,即获取引用但不允许改变引用目的地。 The & operator borrows ownership from a value. &运算符从值中借用所有权。

  2. Borrow mutably , ie, take a reference to mutate the destination.可变地借用,即获取一个引用来改变目的地。 The &mut operator mutably borrows ownership from a value. &mut运算符可变地从值借用所有权。

The Rust documentation about borrowing rules says: 关于借用规则Rust 文档说:

First, any borrow must last for a scope no greater than that of the owner.首先,任何借用的持续范围不得超过所有者的范围。 Second, you may have one or the other of these two kinds of borrows, but not both at the same time:其次,您可能拥有这两种借款中的一种,但不能同时拥有:

  • one or more references ( &T ) to a resource,对资源的一个或多个引用( &T ),
  • exactly one mutable reference ( &mut T ).正好是一个可变引用( &mut T )。

I believe that taking a reference is creating a pointer to the value and accessing the value by the pointer.我相信引用是创建一个指向该值的指针并通过该指针访问该值。 This could be optimized away by the compiler if there is a simpler equivalent implementation.如果有更简单的等效实现,这可以由编译器优化掉。

However, I don't understand what move means and how it is implemented.但是,我不明白移动意味着什么以及它是如何实施的。

For types implementing the Copy trait it means copying eg by assigning the struct member-wise from the source, or a memcpy() .对于实现Copy trait 的类型,它意味着复制,例如通过从源或memcpy()分配结构成员。 For small structs or for primitives this copy is efficient.对于小型结构或原语,此副本是有效的。

And for move ?而对于移动

This question is not a duplicate of What are move semantics?这个问题不是什么是移动语义的重复 because Rust and C++ are different languages and move semantics are different between the two.因为 Rust 和 C++ 是不同的语言,并且两者之间的移动语义不同。

Semantics语义

Rust implements what is known as an Affine Type System : Rust 实现了所谓的仿射类型系统

Affine types are a version of linear types imposing weaker constraints, corresponding to affine logic.仿射类型是线性类型的一个版本,它施加较弱的约束,对应于仿射逻辑。 An affine resource can only be used once , while a linear one must be used once.仿射资源只能使用一次,而线性资源必须使用一次。

Types that are not Copy , and are thus moved, are Affine Types: you may use them either once or never, nothing else.不是Copy并因此被移动的类型是仿射类型:您可以使用它们一次或从不使用它们,没有别的。

Rust qualifies this as a transfer of ownership in its Ownership-centric view of the world (*). Rust 在其以所有权为中心的世界观 (*) 中将此视为所有权转移

(*) Some of the people working on Rust are much more qualified than I am in CS, and they knowingly implemented an Affine Type System; (*) 一些在 Rust 工作的人比我在 CS 中更有资格,他们故意实施了仿射类型系统; however contrary to Haskell which exposes the math-y/cs-y concepts, Rust tends to expose more pragmatic concepts.然而,与 Haskell 公开 math-y/cs-y 概念相反,Rust 倾向于公开更实用的概念。

Note: it could be argued that Affine Types returned from a function tagged with #[must_use] are actually Linear Types from my reading.注意:可以说,从标记为#[must_use]的函数返回的仿射类型实际上是我阅读中的线性类型。


Implementation执行

It depends.这取决于。 Please keep in mind than Rust is a language built for speed, and there are numerous optimizations passes at play here which will depend on the compiler used (rustc + LLVM, in our case).请记住,Rust 是一种为速度而构建的语言,这里有许多优化通道在起作用,这取决于所使用的编译器(在我们的例子中是 rustc + LLVM)。

Within a function body ( playground ):在函数体 ( playground ) 中:

fn main() {
    let s = "Hello, World!".to_string();
    let t = s;
    println!("{}", t);
}

If you check the LLVM IR (in Debug), you'll see:如果您检查 LLVM IR(在调试中),您将看到:

%_5 = alloca %"alloc::string::String", align 8
%t = alloca %"alloc::string::String", align 8
%s = alloca %"alloc::string::String", align 8

%0 = bitcast %"alloc::string::String"* %s to i8*
%1 = bitcast %"alloc::string::String"* %_5 to i8*
call void @llvm.memcpy.p0i8.p0i8.i64(i8* %1, i8* %0, i64 24, i32 8, i1 false)
%2 = bitcast %"alloc::string::String"* %_5 to i8*
%3 = bitcast %"alloc::string::String"* %t to i8*
call void @llvm.memcpy.p0i8.p0i8.i64(i8* %3, i8* %2, i64 24, i32 8, i1 false)

Underneath the covers, rustc invokes a memcpy from the result of "Hello, World!".to_string() to s and then to t .在幕后,rustc 从"Hello, World!".to_string()s然后到t的结果调用memcpy While it might seem inefficient, checking the same IR in Release mode you will realize that LLVM has completely elided the copies (realizing that s was unused).虽然它可能看起来效率低下,但在 Release 模式下检查相同的 IR,您会发现 LLVM 已经完全省略了副本(意识到s未使用)。

The same situation occurs when calling a function: in theory you "move" the object into the function stack frame, however in practice if the object is large the rustc compiler might switch to passing a pointer instead.调用函数时会发生相同的情况:理论上您将对象“移动”到函数堆栈框架中,但实际上如果对象很大,rustc 编译器可能会切换为传递指针。

Another situation is returning from a function, but even then the compiler might apply "return value optimization" and build directly in the caller's stack frame -- that is, the caller passes a pointer into which to write the return value, which is used without intermediary storage.另一种情况是从函数返回,但即使如此,编译器也可能应用“返回值优化”并直接在调用者的堆栈帧中构建——也就是说,调用者传递一个指针来写入返回值,该指针不使用中间存储。

The ownership/borrowing constraints of Rust enable optimizations that are difficult to reach in C++ (which also has RVO but cannot apply it in as many cases). Rust 的所有权/借用限制实现了在 C++ 中难以达到的优化(它也有 RVO,但不能在很多情况下应用它)。

So, the digest version:所以,摘要版本:

  • moving large objects is inefficient, but there are a number of optimizations at play that might elide the move altogether移动大型对象效率低下,但有许多优化可能会完全消除移动
  • moving involves a memcpy of std::mem::size_of::<T>() bytes, so moving a large String is efficient because it only copies a couple bytes whatever the size of the allocated buffer they hold onto移动涉及std::mem::size_of::<T>()字节的memcpy ,因此移动大String是有效的,因为它只复制几个字节,无论它们持有的已分配缓冲区的大小如何

When you move an item, you are transferring ownership of that item.当您移动一个项目时,您正在转移该项目的所有权 That's a key component of Rust.这是 Rust 的一个关键组件。

Let's say I had a struct, and then I assign the struct from one variable to another.假设我有一个结构体,然后我将结构体从一个变量分配给另一个变量。 By default, this will be a move, and I've transferred ownership.默认情况下,这将是一个移动,我已经转移了所有权。 The compiler will track this change of ownership and prevent me from using the old variable any more:编译器将跟踪所有权的这种更改并阻止我再使用旧变量:

pub struct Foo {
    value: u8,
}

fn main() {
    let foo = Foo { value: 42 };
    let bar = foo;

    println!("{}", foo.value); // error: use of moved value: `foo.value`
    println!("{}", bar.value);
}

how it is implemented.它是如何实施的。

Conceptually, moving something doesn't need to do anything.从概念上讲,移动某些东西不需要做任何事情。 In the example above, there wouldn't be a reason to actually allocate space somewhere and then move the allocated data when I assign to a different variable.在上面的例子中,当我分配给不同的变量时,没有理由在某处实际分配空间然后移动分配的数据。 I don't actually know what the compiler does, and it probably changes based on the level of optimization.我实际上不知道编译器做了什么,它可能会根据优化级别而变化。

For practical purposes though, you can think that when you move something, the bits representing that item are duplicated as if via memcpy .但出于实际目的,您可以认为当您移动某物时,表示该项目的位被复制,就像通过memcpy This helps explain what happens when you pass a variable to a function that consumes it, or when you return a value from a function (again, the optimizer can do other things to make it efficient, this is just conceptually):这有助于解释当你将一个变量传递给一个使用它的函数时会发生什么,或者当你从一个函数中返回一个值时会发生什么(同样,优化器可以做其他事情来提高效率,这只是概念上的):

// Ownership is transferred from the caller to the callee
fn do_something_with_foo(foo: Foo) {} 

// Ownership is transferred from the callee to the caller
fn make_a_foo() -> Foo { Foo { value: 42 } } 

"But wait!", you say, " memcpy only comes into play with types implementing Copy !". “但是等等!”,你说,“ memcpy只对实现Copy类型起作用!”。 This is mostly true, but the big difference is that when a type implements Copy , both the source and the destination are valid to use after the copy!这大部分是正确的,但最大的区别在于,当一个类型实现Copy目标在复制后都可以使用!

One way of thinking of move semantics is the same as copy semantics, but with the added restriction that the thing being moved from is no longer a valid item to use.移动语义的一种思考方式与复制语义相同,但增加了一个限制,即被移动的事物不再是一个有效的项目。

However, it's often easier to think of it the other way: The most basic thing that you can do is to move / give ownership away, and the ability to copy something is an additional privilege.然而,换一种方式来考虑它通常更容易:您可以做的最基本的事情是移动/放弃所有权,复制某些东西的能力是一种额外的特权。 That's the way that Rust models it.这就是 Rust 建模的方式。

This is a tough question for me!这对我来说是一个棘手的问题! After using Rust for a while the move semantics are natural.使用 Rust 一段时间后,移动语义很自然。 Let me know what parts I've left out or explained poorly.让我知道我遗漏了哪些部分或解释得不好。

Please let me answer my own question.请让我回答我自己的问题。 I had trouble, but by asking a question here I did Rubber Duck Problem Solving .我遇到了麻烦,但通过在这里提出问题,我完成了橡皮鸭问题解决 Now I understand:现在我明白了:

A move is a transfer of ownership of the value.移动是价值所有权转移

For example the assignment let x = a;例如赋值let x = a; transfers ownership: At first a owned the value.所有权转移:首先a拥有价值。 After the let it's x who owns the value.let之后是x拥有该值。 Rust forbids to use a thereafter. Rust 禁止在此之后使用a

In fact, if you do println!("a: {:?}", a);事实上,如果你执行println!("a: {:?}", a); after the let the Rust compiler says:let Rust 编译器说:

error: use of moved value: `a`
println!("a: {:?}", a);
                    ^

Complete example:完整示例:

#[derive(Debug)]
struct Example { member: i32 }

fn main() {
    let a = Example { member: 42 }; // A struct is moved
    let x = a;
    println!("a: {:?}", a);
    println!("x: {:?}", x);
}

And what does this move mean?而这一举动又意味着什么呢?

It seems that the concept comes from C++11.这个概念似乎来自 C++11。 A document about C++ move semantics says:关于 C++ 移动语义的文档说:

From a client code point of view, choosing move instead of copy means that you don't care what happens to the state of the source.从客户端代码的角度来看,选择移动而不是复制意味着您不关心源的状态会发生什么。

Aha.啊哈。 C++11 does not care what happens with source. C++11 不关心源代码会发生什么。 So in this vein, Rust is free to decide to forbid to use the source after a move.因此,在这种情况下,Rust 可以自由决定禁止在移动后使用源。

And how it is implemented?以及它是如何实施的?

I don't know.我不知道。 But I can imagine that Rust does literally nothing.但我可以想象 Rust 什么都不做。 x is just a different name for the same value. x只是相同值的不同名称。 Names usually are compiled away (except of course debugging symbols).名称通常被编译掉(当然调试符号除外)。 So it's the same machine code whether the binding has the name a or x .因此,无论绑定的名称为a还是x它都是相同的机器代码。

It seems C++ does the same in copy constructor elision.似乎 C++ 在复制构造函数省略中做同样的事情。

Doing nothing is the most efficient possible.什么都不做是最有效率的。

Passing a value to function, also results in transfer of ownership;将值传递给函数,也会导致所有权的转移; it is very similar to other examples:它与其他示例非常相似:

struct Example { member: i32 }

fn take(ex: Example) {
    // 2) Now ex is pointing to the data a was pointing to in main
    println!("a.member: {}", ex.member) 
    // 3) When ex goes of of scope so as the access to the data it 
    // was pointing to. So Rust frees that memory.
}

fn main() {
    let a = Example { member: 42 }; 
    take(a); // 1) The ownership is transfered to the function take
             // 4) We can no longer use a to access the data it pointed to

    println!("a.member: {}", a.member);
}

Hence the expected error:因此预期的错误:

post_test_7.rs:12:30: 12:38 error: use of moved value: `a.member`

Rust's move keyword always bothers me so, I decided to write my understanding which I obtained after discussion with my colleagues. Rust 的move关键字一直困扰着我,所以我决定写下我在与同事讨论后获得的理解。

I hope this might help someone.我希望这可以帮助某人。

let x = 1;

In the above statement, x is a variable whose value is 1. Now,在上面的语句中,x 是一个值为 1 的变量。现在,

let y = || println!("y is a variable whose value is a closure");

So, move keyword is used to transfer the ownership of a variable to the closure.因此, move关键字用于将变量的所有权转移到闭包。

In the below example, without move , x is not owned by the closure.在下面的例子中,没有movex不属于闭包。 Hence x is not owned by y and available for further use.因此x不属于y并且可供进一步使用。

let x = 1;
let y = || println!("this is a closure that prints x = {}". x);

On the other hand, in this next below case, the x is owned by the closure.另一方面,在下面这个例子中, x归闭包所有。 x is owned by y and not available for further use. xy ,不可进一步使用。

let x = 1;
let y = move || println!("this is a closure that prints x = {}". x);

By owning I mean containing as a member variable . owning我的意思是containing as a member variable The example cases above are in the same situation as the following two cases.上面的示例案例与以下两种情况处于相同的情况。 We can also assume the below explanation as to how the Rust compiler expands the above cases.我们还可以假设以下关于 Rust 编译器如何扩展上述情况的解释。

The formar (without move ; ie no transfer of ownership),形式(没有move ;即没有所有权转让),

struct ClosureObject {
    x: &u32
}

let x = 1;
let y = ClosureObject {
    x: &x
};

The later (with move ; ie transfer of ownership),后者( move ;即所有权转让),

struct ClosureObject {
    x: u32
}

let x = 1;
let y = ClosureObject {
    x: x
};

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

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