Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does Rust provide move semantics?

The Rust language website claims move semantics as one of the features of the language. But I can't see how move semantics is implemented in Rust.

Rust boxes are the only place where move semantics are used.

let x = Box::new(5); let y: Box<i32> = x; // x is 'moved' 

The above Rust code can be written in C++ as

auto x = std::make_unique<int>(); auto y = std::move(x); // Note the explicit move 

As far as I know (correct me if I'm wrong),

  • Rust doesn't have constructors at all, let alone move constructors.
  • No support for rvalue references.
  • No way to create functions overloads with rvalue parameters.

How does Rust provide move semantics?

like image 773
user3335 Avatar asked Apr 07 '15 11:04

user3335


People also ask

Does rust have move semantics?

Rust requires implementations for clone, but for all moves, the implementation is the same: copy the memory in the value itself, and don't call the destructor on the original value. And in Rust, all types are movable with this exact implementation – non-movable types don't exist (though non-movable values do).

What is std move?

std::move is used to indicate that an object t may be "moved from", i.e. allowing the efficient transfer of resources from t to another object. In particular, std::move produces an xvalue expression that identifies its argument t . It is exactly equivalent to a static_cast to an rvalue reference type.

What is Move semantics in C++?

Move semantics is about transferring resources rather than copying them when nobody needs the source value anymore. In C++03, objects are often copied, only to be destroyed or assigned-over before any code uses the value again.


2 Answers

I think it's a very common issue when coming from C++. In C++ you are doing everything explicitly when it comes to copying and moving. The language was designed around copying and references. With C++11 the ability to "move" stuff was glued onto that system. Rust on the other hand took a fresh start.


Rust doesn't have constructors at all, let alone move constructors.

You do not need move constructors. Rust moves everything that "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's default constructor is (by convention) simply an associated function called new:

struct A(i32); impl A {     fn new() -> A {         A(5)     } } 

More complex constructors should have more expressive names. This is the named constructor idiom in C++


No support for rvalue references.

It has always been a requested feature, see RFC issue 998, but most likely you are asking for a different feature: moving stuff to functions:

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.

You can do that with traits.

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" } 
like image 175
oli_obk Avatar answered Sep 22 '22 20:09

oli_obk


Rust's moving and copying semantics are very different from C++. I'm going to take a different approach to explain them than the existing answer.


In C++, copying is an operation that can be arbitrarily complex, due to custom copy constructors. Rust doesn't want custom semantics of simple assignment or argument passing, and so takes a different approach.

First, an assignment or argument passing in Rust is always just a simple memory copy.

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) 

But what if the object controls some resources? Let's say we are dealing with a simple smart pointer, Box.

let b1 = Box::new(42); let b2 = b1; 

At this point, if just the bytes are copied over, wouldn't the destructor (drop in Rust) be called for each object, thus freeing the same pointer twice and causing undefined behavior?

The answer is that Rust moves by default. This means that it copies the bytes to the new location, and the old object is then gone. It is a compile error to access b1 after the second line above. And the destructor is not called for it. The value was moved to b2, and b1 might as well not exist anymore.

This is how move semantics work in Rust. The bytes are copied over, and the old object is gone.

In some discussions about C++'s move semantics, Rust's way was called "destructive move". There have been proposals to add the "move destructor" or something similar to C++ so that it can have the same semantics. But move semantics as they are implemented in C++ don't do this. The old object is left behind, and its destructor is still called. Therefore, you need a move constructor to deal with the custom logic required by the move operation. Moving is just a specialized constructor/assignment operator that is expected to behave in a certain way.


So by default, Rust's assignment moves the object, making the old location invalid. But many types (integers, floating points, shared references) have semantics where copying the bytes is a perfectly valid way of creating a real copy, with no need to ignore the old object. Such types should implement the Copy trait, which can be derived by the compiler automatically.

#[derive(Copy)] struct JustTwoInts {   one: i32,   two: i32, } 

This signals the compiler that assignment and argument passing do not invalidate the old object:

let j1 = JustTwoInts { one: 1, two: 2 }; let j2 = j1; println!("Still allowed: {}", j1.one); 

Note that trivial copying and the need for destruction are mutually exclusive; a type that is Copy cannot also be Drop.


Now what about when you want to make a copy of something where just copying the bytes isn't enough, e.g. a vector? There is no language feature for this; technically, the type just needs a function that returns a new object that was created the right way. But by convention this is achieved by implementing the Clone trait and its clone function. In fact, the compiler supports automatic derivation of Clone too, where it simply clones every field.

#[Derive(Clone)] struct JustTwoVecs {   one: Vec<i32>,   two: Vec<i32>, }  let j1 = JustTwoVecs { one: vec![1], two: vec![2, 2] }; let j2 = j1.clone(); 

And whenever you derive Copy, you should also derive Clone, because containers like Vec use it internally when they are cloned themselves.

#[derive(Copy, Clone)] struct JustTwoInts { /* as before */ } 

Now, are there any downsides to this? Yes, in fact there is one rather big downside: because moving an object to another memory location is just done by copying bytes, and no custom logic, a type cannot have references into itself. In fact, Rust's lifetime system makes it impossible to construct such types safely.

But in my opinion, the trade-off is worth it.

like image 31
Sebastian Redl Avatar answered Sep 21 '22 20:09

Sebastian Redl