Rust language website声明将语义作为该语言的一项功能。但我看不出Rust中是如何实现移动语义的。
Rust box是唯一使用移动语义的地方。
let x = Box::new(5);
let y: Box<i32> = x; // x is 'moved'
上面的Rust代码可以用C ++编写为
auto x = std::make_unique<int>();
auto y = std::move(x); // Note the explicit move
据我所知(如果我错了,请纠正我),
Rust如何提供移动语义?
答案 0 :(得分:37)
我认为这是来自C ++时非常常见的问题。在C ++中,当涉及到复制和移动时,你正在做一切事情。该语言是围绕复制和引用而设计的。使用C ++ 11,能够移动&#34;东西粘在那个系统上。另一方面,Rust重新开始了。
Rust根本没有构造函数,更不用说移动构造函数了。
您不需要移动构造函数。 Rust会移动&#34;没有复制构造函数的所有东西&#34;,a.k.a。&#34;没有实现Copy
特征&#34;。
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 issue 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
}
无法使用rvalue参数创建函数重载。
你可以用特质做到这一点。
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"
}
答案 1 :(得分:7)
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的方式被称为“破坏性移动”#34;有人建议添加&#34;移动析构函数&#34;或类似于C ++的东西,以便它可以具有相同的语义。但是移动语义因为它们是用C ++实现的,所以不要这样做。旧对象被遗忘,其析构函数仍然被调用。因此,您需要一个移动构造函数来处理移动操作所需的自定义逻辑。移动只是一个专门的构造函数/赋值运算符,预计会以某种方式运行。
因此默认情况下,Rust的赋值会移动对象,使旧位置无效。但是许多类型(整数,浮点,共享引用)具有语义,其中复制字节是创建实际副本的完全有效的方式,而不需要忽略旧对象。这些类型应该实现Copy
特征,它可以由编译器自动派生。
#[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
特征及其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 */ }
现在,这有什么缺点吗?是的,事实上有一个相当大的缺点:因为将对象移动到另一个内存位置只是通过复制字节来完成,而没有自定义逻辑,类型cannot have references into itself。实际上,Rust的终生系统使得无法安全地构建这种类型。
但在我看来,权衡是值得的。
答案 2 :(得分:0)
Rust支持具有以下功能的移动语义:
所有类型都是可移动的。
默认情况下,在整个语言中将值发送到某处都是一种动作。对于非Copy
类型,例如Vec
,以下是所有动作在Rust中:按值传递参数,返回值,赋值,按值进行模式匹配。
Rust中没有std::move
,因为它是默认设置。您确实一直在使用移动。
必须知道不得使用已移动的值。如果您拥有值x: String
并执行channel.send(x)
,则将值发送到另一个线程,则编译器知道x
已被移动。移动后尝试使用它是一个编译时错误,“使用移动值”。而且,如果有人引用了该值(悬挂的指针),则无法移动该值。
鲁斯不知道不要在移动的值上调用析构函数。移动值可以转移所有权,包括清理责任。类型不必表示特殊的“值已移动”状态。
移动便宜,并且性能可预测。基本上是memcpy。返回巨大的Vec
总是非常快-您只需复制三个词即可。
Rust标准库在任何地方都使用并支持移动。我已经提到了使用移动语义在线程之间安全地转移值所有权的通道。其他方面还不错:所有类型都支持Rust中的无副本std::mem::swap()
; Into
和From
标准转换特征是按值; Vec
和其他集合具有.drain()
和.into_iter()
方法,因此您可以粉碎一个数据结构,将所有值移出该结构,然后使用这些值构建新的结构。
Rust没有移动引用,但是移动是Rust中一个强大而核心的概念,它提供了许多与C ++相同的性能优势,以及其他一些优势。
答案 3 :(得分:0)
我想补充一点,没有必要移动到 memcpy
。如果堆栈上的对象足够大,Rust 的编译器可能会选择传递对象的指针。
答案 4 :(得分:-2)
在C ++中,类和结构的默认分配是浅表复制。将复制值,但不复制指针引用的数据。因此,修改一个实例会更改所有副本的引用数据。在其他情况下,这些值(例如用于管理的值)保持不变,可能会导致状态不一致。动作语义避免了这种情况。具有移动语义的内存托管容器的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