什么是Rust中的移动语义?

时间:2015-05-17 15:46:14

标签: rust move-semantics ownership

在Rust中,有两种可能性来引用

  1. 借用,即参考但不允许改变参考目的地。 &运算符从值中借用所有权。

  2. 可以借用,即参考改变目的地。 &mut运算符可以从值中可变地借用所有权。

  3. Rust documentation about borrowing rules说:

      

    首先,任何借用必须持续不超过的范围   所有者。其次,你可能有这两种中的一种或另一种   借用,但不能同时借两个:

         
        
    • 对资源的一个或多个引用(&T),
    •   
    • 恰好是一个可变引用(&mut T)。
    •   

    我认为引用引用是创建指向值的指针并通过指针访问值。如果存在更简单的等效实现,则编译器可以对此进行优化。

    但是,我不明白移动的含义及其实现方式。

    对于实现Copy特征的类型,它意味着复制,例如通过从源分配结构成员,或memcpy()。对于小结构或基元,这个副本是有效的。

    移动

    这个问题不是What are move semantics?的重复,因为Rust和C ++是不同的语言,两者之间的移动语义不同。

5 个答案:

答案 0 :(得分:18)

<强>语义

Rust实现了所谓的Affine Type System

  

仿射类型是强加较弱约束的线性类型的一种形式,对应于仿射逻辑。 仿射资源只能使用一次,而线性资源只能使用一次。

Copy并且因此被移动的类型是仿射类型:您可以使用它们一次或从不使用它们。

Rust在其以所有权为中心的世界观(*)中将其视为所有权转移

(*)一些在Rust工作的人比在CS中更有资格,他们故意实施仿射型系统;然而,与公开math-y / cs-y概念的Haskell相反,Rust倾向于暴露出更实用的概念。

注意:可以认为从#[must_use]标记的函数返回的仿射类型实际上是我阅读时的线性类型。

<强>实施

这取决于。请记住,Rust是一种为速度而构建的语言,这里有许多优化过程,这取决于所使用的编译器(在我们的例子中是rustc + LLVM)。

在函数体(playground)中:

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

如果你检查LLVM IR(在Debug中),你会看到:

%_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)

在封面下方,rustc从memcpy"Hello, World!".to_string()的结果调用s,然后调用t。虽然看起来效率低下,但在发布模式下检查相同的IR,您会发现LLVM已经完全省略了副本(意识到s未被使用)。

调用函数时会出现同样的情况:理论上你将对象“移动”到函数堆栈框架中,但实际上如果对象很大,则rustc编译器可能会切换到传递指针。

另一种情况是从函数返回,但即使这样,编译器也可以应用“返回值优化”并直接在调用者的堆栈帧中构建 - 也就是说,调用者传递一个指针写入返回值,在没有中间存储的情况下使用。

Rust的所有权/借用限制使得在C ++中很难实现优化(也有RVO,但在很多情况下都不能应用它)。

所以,摘要版本:

  • 移动大型物体效率低下,但有许多优化措施可能会完全忽略此举动
  • 移动涉及memcpystd::mem::size_of::<T>()字节,因此移动大String是有效的,因为它只有几个字节,无论它们保留的分配缓冲区的大小是什么

答案 1 :(得分:10)

当您移动某个项目时,您转移该项目的所有权。这是Rust的一个关键组成部分。

让我们说我有一个结构,然后我将结构从一个变量分配给另一个变量。默认情况下,这将是一个举动,我已转让所有权。编译器将跟踪此所有权更改并阻止我再使用旧变量:

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);
}
  

如何实施。

从概念上讲,移动某些东西并不需要来做任何事情。在上面的例子中,当我分配给不同的变量时,实际上在某处实际分配空间然后移动分配的数据是没有理由的。我实际上并不知道编译器的作用,它可能会根据优化级别而改变。

但是出于实际目的,您可以认为当您移动某些内容时,表示该项目的位将被复制,就像通过memcpy一样。这有助于解释当您将变量传递给使用它的函数时,或者当您从函数返回值时(再次,优化器可以执行其他操作以使其高效)时会发生什么,这只是概念上):

// 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 } } 

&#34;但是等等!&#34;,你说,&#34; memcpy只对实现Copy的类型发挥作用!&#34;。这大部分都是正确的,但最大的区别在于,当类型实现Copy时, source destination 都可以在复制后使用!< / p>

一种思考移动语义的方法与复制语义相同,但附加的限制是移动的东西不再是要使用的有效项目。

但是,从另一个角度来看,通常更容易想到它:你可以做的最基本的事情是移动/放弃所有权,复制某些东西的能力是另一种特权。这就是Rust模仿它的方式。

这对我来说是一个棘手的问题!使用Rust一段时间后,移动语义是很自然的。让我知道我遗漏或解释不好的部分。

答案 2 :(得分:1)

请让我回答我自己的问题。我遇到了麻烦,但在这里问了一个问题我做了Rubber Duck Problem Solving。现在我明白了:

移动是值的所有权转移

例如,作业let x = a;转让所有权:首先a拥有该值。拥有该值的let之后x。 Rust禁止此后使用a

事实上,如果你在Rust编译器println!("a: {:?}", a);之后执行let

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

完整示例:

#[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);
}

移动意味着什么?

似乎这个概念来自C ++ 11。 document about C++ move semantics说:

  

从客户端代码的角度来看,选择move而不是copy意味着你不关心源的状态会发生什么。

啊哈。 C ++ 11并不关心源代码会发生什么。因此,在这种情况下,Rust可以自由决定在移动后禁止使用源。

它是如何实现的?

我不知道。但是我可以想象Rust确实没什么。 x只是同一个值的不同名称。名称通常被编译掉(当然除了调试符号)。因此绑定的名称为ax是相同的机器代码。

似乎C ++在复制构造函数elision中也是如此。

什么都不做是最有效的。

答案 3 :(得分:1)

将值传递给函数,也会导致所有权转移;它与其他例子非常相似:

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);
}

因此预期错误:

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

答案 4 :(得分:0)

Rust 的 move 关键字一直困扰着我,所以我决定写下我在与同事讨论后获得的理解。

我希望这可以帮助某人。

let x = 1;

在上面的语句中,x 是一个值为 1 的变量。现在,

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

因此,move 关键字用于将变量的所有权转移给闭包。

在下面的示例中,如果没有 move,则 x 不属于闭包。因此,x 不归 y 所有,可供进一步使用。

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

另一方面,在下面这个例子中,x 归闭包所有。 xy 所有,不可进一步使用。

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

owning 我的意思是 containing as a member variable。上面的示例案例与以下两种情况处于相同的情况。我们也可以假设下面的解释是关于 Rust 编译器如何扩展上述情况的。

形式(没有move;即没有所有权转让),

struct ClosureObject {
    x: &u32
}

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

后者(带move;即所有权转让),

struct ClosureObject {
    x: u32
}

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