什么是Rust中的移动语义?

nal*_*ply 21 ownership move-semantics rust

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

  1. 借用,即参考,但不允许改变参考目的地.该&运营商借用值所有权.

  2. 可变地借用,即参考改变目的地.该&mut运营商性情不定地借用一个值所有权.

有关借用规则Rust文档说:

首先,任何借入必须持续不超过所有者的范围.其次,您可能拥有这两种借款中的一种或另一种,但不能同时使用这两种:

  • 一个或多个&T资源的引用(),
  • 一个可变的引用(&mut T).

我相信引用一个引用是创建一个指向值的指针并通过指针访问该值.如果存在更简单的等效实现,则编译器可以优化它.

但是,我不明白什么是移动的意思以及它是如何实现的.

对于实现Copy特征的类型,它意味着复制,例如通过从源分配结构成员,或者a memcpy().对于小结构或原始数据,此副本是有效的.

而对于

这个问题不是什么是移动语义的重复因为Rust和C++是不同的语言,移动语义在两者之间是不同的.

Mat*_* M. 21

语义

Rust实现了所谓的仿射类型系统:

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

没有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);
}
Run Code Online (Sandbox Code Playgroud)

如果检查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)
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

那么,摘要版本:

  • 移动大型物体效率低下,但有许多优化措施可能会完全忽略此举
  • 活动涉及memcpystd::mem::size_of::<T>()字节,因此移动大String是有效的,因为它只有一对夫妇字节不管他们守住分配的缓冲区的大小

  • 仿射类型的用例是什么?为什么防止多次使用一个值很有用? (2认同)
  • @weberc2:假设您有一个 `String`,其内容存储在堆上。通过*移动*它,您可以避免引用计数或垃圾回收的需要,因为没有人可以访问“旧”绑定。好吧,事实证明,当您希望避免重用“旧”绑定时,还有其他用例。例如,在状态机中,一旦您从一个状态转换出来,为该状态转换 *再次* 就没有意义了。移动语义有助于在编译时对此进行建模。 (2认同)
  • @ weberc2:*特别是,我敢肯定,我可以用一种没有仿射类型的语言制作一个静态保证的状态机* =&gt;如果您要进行管理,我真的很想知道如何(我已经在工作中使用C ++,并且可以使用更多的静态保证)。至于`func1`,`func2`,请注意,在仿射类型系统中,您会遇到编译时错误,因此不存在使用后可用的问题。解决方案是通过引用传递,或让`func1`返回字符串。前者要求借阅检查是安全的,后者则需要一些扭曲。 (2认同)

She*_*ter 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);
}
Run Code Online (Sandbox Code Playgroud)

它是如何实现的.

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

但是出于实际目的,您可以认为当您移动某些内容时,表示该项目的位将被复制,就像通过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 } } 
Run Code Online (Sandbox Code Playgroud)

"但是等等!",你说," memcpy只有实现类型才能发挥作用Copy!".这大部分都是正确的,但最大的区别在于,当一个类型实现时Copy,目标都可以在复制后使用!

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

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

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

  • 我还补充说,不仅从变量中移出来阻止它的进一步使用,它还禁止在这个变量上运行析构函数.这是与C++的一个重要区别,其中AFAIK类型必须明确设计为允许移动,因为它们的析构函数将始终运行,因此移动构造函数必须确保析构函数不会做任何愚蠢的事情. (4认同)
  • @nalply是的,C++ 11肯定使用了名称"移动语义"并正式将其引入语言,但移动语义的*概念*已经存在了很长时间.但是,这是程序员必须手动跟踪的东西.Rust将这个棘手的主题推广给一等公民,让自己更难以自拔,这对我来说是一个非常有趣的语言部分! (4认同)
  • @ kirbyfan64sos是迂腐的,移动*总是*复制值(关于我在帖子中说的优化器的警告).但是,具有堆分配组件(`Box`,`Vec`,`String`,依此类推)的值是使用概念上具有指向数据的指针的结构构建的.*指针*被复制,*指向*数据不是(这是'克隆'特征的领域).你的观点是大分配没有被移动是正确的. (3认同)
  • @Shepmaster:是的,与C++相反,Rust实现了[仿射类型系统](http://en.wikipedia.org/wiki/Substructural_type_system#Affine_type_systems),值最多只能使用(或"消耗")***.由于这一点,Rust允许实现由编译器检查类型的"状态机",并且我已经看到许多库已经利用了它. (2认同)