I have three questions regarding move semantics in Rust.
Not long ago I learned what move semantics are in C++ and I realized that move semantics happen almost all the time in Rust and it happens by default. For what I understand, move semantics in C++ have the purpose of reducing deep copying, or reducing memory allocations on the heap by transferring ownership from one variable to another. I understand that when you use the =
operator in C++, there is always something copied. (I'm talking about debug builds only for now.)
Let me give an example in C++ first:
struct MyStruct {
int a;
float b;
bool c;
float* d;
MyStruct() : a(0), b(0.0f), c(false) {
d = new float[10];
}
~MyStruct() { delete[] d; }
MyStruct(MyStruct& other) {
a = other.a;
b = other.b;
c = other.c;
d = new float[10];
memcpy(d, other.d, sizeof(float) * 10);
}
MyStruct(MyStruct&& other) {
a = other.a;
b = other.b;
c = other.c;
d = other.d;
// For what I understand, this line is the big thing that makes move assignments possible
other.d = nullptr;
}
};
int main() {
MyStruct ms;
// Deep copy, so it's like clone() in Rust
MyStruct copy = ms;
// Both structs are valid
}
Now I want to make a move, not a copy:
int main() {
MyStruct ms;
MyStruct move = std::move(ms);
// ms is invalid now
}
For what I undestand, moving ms
into copy
means shallow copying the ms
struct including the pointer with the heap data and setting the ms
's pointer to nullptr
. This means that when ms
is dropped, delete[]
does nothing but a check for null.
Now, in Rust this is almost the same example:
#[derive(Clone)]
struct MyStruct {
a: i32,
b: f32,
c: bool,
d: Vec<f32>,
}
fn main() {
let ms = MyStruct { a: 19, b: 3.141, c: true, d: Vec::new() };
let copy = ms.clone();
// Both structs are valid
}
With move:
fn main() {
let ms = MyStruct { a: 19, b: 3.141, c: true, d: Vec::new() };
let move = ms;
// ms is invalid now
}
My question is: How do move semantics actually work in Rust? In the same way as in C++?.
Since the compiler knows that ms
is invalid after the move, what I believe is that the compiler can do some small optimizations by not calling the deallocator on ms
's heap data pointer in the Vec
, because that heap data is moved. In C++, when the old variable is destroyed, the destructor is called and delete
is called, which means a function call and an if statement inside that function. Rust should be free to not call any function. This means that, technically, Rust is more performant that C++. (C++ is slower than Rust by a function call and an if statement, not a big deal anyway.) My second question: Is this assumption true? Does rustc make these optimizations?
And one more thing. In C++, I think that doing a move is useless when you have a struct with no heap data, because it will do a shallow copy anyway, so it's the same as a deep copy:
struct MyStruct {
long a1;
long a2;
long a3;
// ... Lots of fields
};
struct MyStruct {
a1: i64,
a2: i64,
a3: i64,
// ... Lots of fields
}
And my third question, which is tied to the first one: When you do a move in Rust, does it do a shallow copy? Even on large structs?. I think not.
As I write this, I realize that maybe the second and third questions are irrelevant, because the code will anyway be optimized in release builds. I hope that I was clear enough... And I hope that this post helps a bit.