Rust 如何提供移动语义?
How does Rust provide move semantics?
Rust language website 声称移动语义是该语言的特征之一。但是我看不到 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,"move" 东西的能力被粘在了那个系统上。另一方面,Rust 重新开始。
Rust doesn't have constructors at all, let alone move constructors.
您不需要移动构造函数。 Rust 会移动 "does not have a copy constructor"、a.k.a 的所有内容。 "does not implement the 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++ 中的命名构造函数习语
No support for rvalue references.
它一直是一个请求的功能,请参阅 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
}
No way to create functions overloads with rvalue parameters.
你可以用特征来做到这一点。
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 的方式被称为 "destructive move"。已经有人提议添加 "move destructor" 或类似于 C++ 的东西,以便它可以具有相同的语义。但是在 C++ 中实现的移动语义不会这样做。旧对象被留下,它的析构函数仍然被调用。因此,您需要一个移动构造函数来处理移动操作所需的自定义逻辑。移动只是一个专门的 constructor/assignment 运算符,预计会以某种方式运行。
所以默认情况下,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
.
现在,当您想复制仅复制字节是不够的东西时怎么办?向量?没有语言功能;从技术上讲,该类型只需要一个函数 returns 一个以正确方式创建的新对象。但按照惯例,这是通过实现 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 */ }
现在,这有什么缺点吗?是的,事实上有一个相当大的缺点:因为将对象移动到另一个内存位置只是通过复制字节来完成的,并且没有自定义逻辑,类型 。事实上,Rust 的生命周期系统使得安全地构造此类类型成为不可能。
但在我看来,这种权衡是值得的。
在 C++ 中,类 和结构的默认赋值是浅拷贝。复制值,但不复制指针引用的数据。因此修改一个实例会更改所有副本的引用数据。值(f.e。用于管理)在另一个实例中保持不变,可能呈现不一致状态。移动语义避免了这种情况。具有移动语义的内存管理容器的 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
Rust 支持具有以下功能的移动语义:
所有类型均可移动
默认情况下,在某处发送一个值是整个语言中的一个动作。对于非Copy
类型,例如Vec
],以下是 Rust 中的所有动作:按值传递参数、返回值、赋值、按值进行模式匹配。
Rust 中没有 std::move
,因为它是默认设置。你真的一直在用招式
Rust 知道不能使用移动的值。 如果你有一个值 x: String
并执行 channel.send(x)
,发送值传递给另一个线程,编译器知道 x
已被移动。移动后尝试使用它是编译时错误,"use of moved value"。如果有人引用了某个值(悬空指针),则您无法移动该值。
Rust 知道不要对移动的值调用析构函数。 移动值会转移所有权,包括清理责任。类型不必能够表示特殊的 "value was moved" 状态。
移动便宜,性能可预测。它基本上是 memcpy。返回一个巨大的 Vec
总是很快的——你只是复制三个单词。
Rust 标准库在任何地方都使用并支持移动。 我已经提到过通道,它使用移动语义来跨线程安全地转移值的所有权。其他不错的地方:所有类型都支持 Rust 中的无副本 std::mem::swap()
; Into
和 From
标准转换特征是按值计算的; Vec
和其他集合具有 .drain()
和 .into_iter()
方法,因此您可以粉碎一个数据结构,将所有值移出它,然后使用这些值构建一个新的。
Rust 没有移动引用,但移动是 Rust 中一个强大的核心概念,提供了许多与 C++ 相同的性能优势,以及一些其他优势。
我想补充一点,没有必要移动到 memcpy
。如果堆栈上的对象足够大,Rust 的编译器可能会选择传递对象的指针。
let s = vec!["udon".to_string(), "ramen".to_string(), "soba".to_string()];
这是它在内存中的表示方式
那我们把s赋值给t
let t = s;
事情是这样的:
let t = s
将向量的三个 header 字段从 s 移动到 t;现在 t 是向量的所有者。向量的元素保持不变
他们在哪里,琴弦也没有任何变化。每个值仍然只有一个所有者。
现在s被释放了,如果我这样写
let u = s
我收到错误消息:“使用移动值:s
”
Rust applies move semantics to almost any use of a value (Except Copy types). Passing
arguments to functions moves ownership to the function’s parameters;
returning a value from a function moves ownership to the caller.
Building a tuple moves the values into the tuple. And so on.
Rust language website 声称移动语义是该语言的特征之一。但是我看不到 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,"move" 东西的能力被粘在了那个系统上。另一方面,Rust 重新开始。
Rust doesn't have constructors at all, let alone move constructors.
您不需要移动构造函数。 Rust 会移动 "does not have a copy constructor"、a.k.a 的所有内容。 "does not implement the 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++ 中的命名构造函数习语
No support for rvalue references.
它一直是一个请求的功能,请参阅 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
}
No way to create functions overloads with rvalue parameters.
你可以用特征来做到这一点。
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 的方式被称为 "destructive move"。已经有人提议添加 "move destructor" 或类似于 C++ 的东西,以便它可以具有相同的语义。但是在 C++ 中实现的移动语义不会这样做。旧对象被留下,它的析构函数仍然被调用。因此,您需要一个移动构造函数来处理移动操作所需的自定义逻辑。移动只是一个专门的 constructor/assignment 运算符,预计会以某种方式运行。
所以默认情况下,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
.
现在,当您想复制仅复制字节是不够的东西时怎么办?向量?没有语言功能;从技术上讲,该类型只需要一个函数 returns 一个以正确方式创建的新对象。但按照惯例,这是通过实现 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 */ }
现在,这有什么缺点吗?是的,事实上有一个相当大的缺点:因为将对象移动到另一个内存位置只是通过复制字节来完成的,并且没有自定义逻辑,类型
但在我看来,这种权衡是值得的。
在 C++ 中,类 和结构的默认赋值是浅拷贝。复制值,但不复制指针引用的数据。因此修改一个实例会更改所有副本的引用数据。值(f.e。用于管理)在另一个实例中保持不变,可能呈现不一致状态。移动语义避免了这种情况。具有移动语义的内存管理容器的 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
Rust 支持具有以下功能的移动语义:
所有类型均可移动
默认情况下,在某处发送一个值是整个语言中的一个动作。对于非
Copy
类型,例如Vec
],以下是 Rust 中的所有动作:按值传递参数、返回值、赋值、按值进行模式匹配。Rust 中没有
std::move
,因为它是默认设置。你真的一直在用招式Rust 知道不能使用移动的值。 如果你有一个值
x: String
并执行channel.send(x)
,发送值传递给另一个线程,编译器知道x
已被移动。移动后尝试使用它是编译时错误,"use of moved value"。如果有人引用了某个值(悬空指针),则您无法移动该值。Rust 知道不要对移动的值调用析构函数。 移动值会转移所有权,包括清理责任。类型不必能够表示特殊的 "value was moved" 状态。
移动便宜,性能可预测。它基本上是 memcpy。返回一个巨大的
Vec
总是很快的——你只是复制三个单词。Rust 标准库在任何地方都使用并支持移动。 我已经提到过通道,它使用移动语义来跨线程安全地转移值的所有权。其他不错的地方:所有类型都支持 Rust 中的无副本
std::mem::swap()
;Into
和From
标准转换特征是按值计算的;Vec
和其他集合具有.drain()
和.into_iter()
方法,因此您可以粉碎一个数据结构,将所有值移出它,然后使用这些值构建一个新的。
Rust 没有移动引用,但移动是 Rust 中一个强大的核心概念,提供了许多与 C++ 相同的性能优势,以及一些其他优势。
我想补充一点,没有必要移动到 memcpy
。如果堆栈上的对象足够大,Rust 的编译器可能会选择传递对象的指针。
let s = vec!["udon".to_string(), "ramen".to_string(), "soba".to_string()];
这是它在内存中的表示方式
那我们把s赋值给t
let t = s;
事情是这样的:
let t = s
将向量的三个 header 字段从 s 移动到 t;现在 t 是向量的所有者。向量的元素保持不变
他们在哪里,琴弦也没有任何变化。每个值仍然只有一个所有者。
现在s被释放了,如果我这样写
let u = s
我收到错误消息:“使用移动值:s
”
Rust applies move semantics to almost any use of a value (Except Copy types). Passing arguments to functions moves ownership to the function’s parameters; returning a value from a function moves ownership to the caller. Building a tuple moves the values into the tuple. And so on.