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.