How does Rust provide move semantics?
Asked Answered
C

6

78

The Rust language website claims move semantics as one of the features of the language. But I can't see how move semantics are 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>(5);
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?

Cm answered 7/4, 2015 at 11:37 Comment(3)
Mostly, where C++ would implicitly copy, Rust implicitly moves. This doesn't only apply to boxes.What
"This language doesn't have any of the brilliant, extremely complicated, error-prone hacks that C++ has to support moves!" You're not wrong... ;-)Fiduciary
Arguably it’s C++ which doesn’t have constructors. What C++ calls ‘constructors’ are actually initializers, and they may have been called that if the term ‘initializer’ wasn’t in turn taken by binding initialization syntax. A true constructor is supposed to return the newly-created value, where C++ ‘constructors’ mutate the object in place. (Compare constructors in functional programming languages, e.g. Haskell or Coq.)Abutting
C
70

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"
}
Casteel answered 7/4, 2015 at 11:50 Comment(15)
So are you actually missing a feature from C++ or is Rust just doing it differently?Casteel
I'm not actually missing any C++ feature, but the way move semantics is implemented is too different. Though Rust's point is to avoid implicit copies, I don't get the idea of "move value types on assignment" when we can use references.Cm
In rust instead of making moving explicit, creating references is explicit: let x = &a; creates a (const) reference named x to a. Also, you should trust the compiler when it comes to optimizations in case you are afraid the implicit moves create a performance penalty. The compiler can optimize a lot due to the move semantics being built into the compiler.Casteel
also, rust still has implicit copies. you just need to implement the Copy trait for your type and it is copied from now on. For a POD, you can even tell the compiler to automatically generate the Copy trait implementation for you.Casteel
"Rust moves everything that isn't copiable" is not a sufficient explanation as to how moving can work without move constructors. How does it, for example, move a structure containing an owning pointer? The most trivial example is the standard Box type. Just raw-copying the bytes of the objects wouldn't work, because destructors would then double-free the managed memory. What gives?Abbreviation
@TheParamagneticCroissant: Rust does not need move constructors that "delete" the previous location, because once you moved out of something, a flag is set that the object should not call Drop::drop. In the future an improved analysis will actually make sure that we don't need such a flag anymore. I'm not sure how much of that has been implemented.Casteel
So once Copy is implemented, you cannot force-move an object/class/whatchamacallit-in-rust?Jueta
@rubenvb: correct, but you can always wrap the object in a wrapper type which does not implement Copy.Casteel
@Jueta You wouldn't want a force-move; Copy implies that a bitwise copy is sufficient to create an independent object, and moves are also just bitwise copies.Kauppi
@KyleStrand Not quite true. IIRC in Rust, move is roughly like copy-initialization of references in C++. In C++, however, move constructors can be customized to have side effects, e.g. making some messages printed on move. On the contrary, Rust allows to customize memory loads by Copy on evaluations of place expressions while C++ treats lvalue-to-rvalue conversions of glvalues mostly no-ops or just volatile loads. It seems that C++'s abstract machine semantics get some more high-level stuff than Rust's way.Kaon
@Kaon I'm sorry, but I don't understand what you're saying, or how it contradicts my statement about bitwise copies. (To be perfectly clear, my comment was only about Rust's behavior.)Kauppi
@KyleStrand I mean that "move" is not necessarily just getting objects copied. There do have different reasons to force-move explicitly, albeit not idiomatic. As an example, in Rust you can't implement customization of side-effectful move as it in C++, where force-move counts. The remaining problem is that I don't catch the idea about the reasons to customize Copy in some strange contexts.Kaon
(About Copy... this should be my fault because I realize that it is not a costomization, but a special mark to the implementation.)Kaon
@Kaon In C++, you're right, move constructors are almost never just bitwise-copies. But in Rust, they really are just bitwise-copies, and there really is no benefit to a move versus a Copy.Kauppi
(Sorry, I mean that user defined move constructors are almost never just copies.)Kauppi
D
56

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(Clone, 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.

Dipietro answered 9/2, 2018 at 13:21 Comment(8)
Would it make sense, to move memory located on the stack, too? Example: rust let i: i32 = 12; let obj = MyStruct(i); allocate space for two i32 variables - means 8 bytes - on the stack. But actual only one is needed after the move in the second line.Lamonicalamont
@Lamonicalamont The compiler might well decide to make this optimization; but it would probably be at the LLVM level, outside of Rust's semantics.Dipietro
@SebastianRedl So in Rust both move and copy is a memcpy with move disallowing original's use. Intelligent, deep copies are delegated to the type author by the Clone trait. Is my understanding correct? Thanks for the answer, your's explains what happens under the covers!Grandam
@Grandam Yes, deep copies must be implemented by Clone. Moves are memcpy.Dipietro
Thank you! Copy trait agrees with my summarization; just putting it here for future readers.Grandam
Best answer IMO. This is very detailed than accepted one.Supervision
This is the best answer. It also cleared my confusion about difference between Rust & C++ move. Rust does bitwise copy on move whereas C++ reuses the same memory. As of Rust 1.51.0, compiler doesn't make this optimization at least in the tests I ran.Eared
"So in Rust both move and copy is a memcpy" a move between memory locations is a trivial memory copy, but the great thing about trivial destructive moves compared to C++ style moves is that the source and destination don't have to be in memory. This means that in rust, unlike C++ managed types can be passed in registers.Callison
F
16

Rust supports move semantics with features like these:

  • All types are moveable.

  • Sending a value somewhere is a move, by default, throughout the language. For non-Copy types, like Vec, the following are all moves in Rust: passing an argument by value, returning a value, assignment, pattern-matching by value.

    You don't have std::move in Rust because it's the default. You're really using moves all the time.

  • Rust knows that moved values must not be used. If you have a value x: String and do channel.send(x), sending the value to another thread, the compiler knows that x has been moved. Trying to use it after the move is a compile-time error, "use of moved value". And you can't move a value if anyone has a reference to it (a dangling pointer).

  • Rust knows not to call destructors on moved values. Moving a value transfers ownership, including responsibility for cleanup. Types don't have to be able to represent a special "value was moved" state.

  • Moves are cheap and the performance is predictable. It's basically memcpy. Returning a huge Vec is always fast—you're just copying three words.

  • The Rust standard library uses and supports moves everywhere. I already mentioned channels, which use move semantics to safely transfer ownership of values across threads. Other nice touches: all types support copy-free std::mem::swap() in Rust; the Into and From standard conversion traits are by-value; Vec and other collections have .drain() and .into_iter() methods so you can smash one data structure, move all the values out of it, and use those values to build a new one.

Rust doesn't have move references, but moves are a powerful and central concept in Rust, providing a lot of the same performance benefits as in C++, and some other benefits as well.

Fiduciary answered 18/12, 2019 at 8:1 Comment(0)
Z
5
let s = vec!["udon".to_string(), "ramen".to_string(), "soba".to_string()];

this is how it is represented in memory

enter image description here

Then let's assign s to t

 let t = s;

this is what happens:

enter image description here

let t = s MOVED the vector’s three header fields from s to t; now t is the owner of the vector. The vector’s elements stayed just where they were, and nothing happened to the strings either. Every value still has a single owner.

Now s is freed, if I write this

  let u = s

I get error: "use of moved value: 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.

Ref for example:Programming Rust by Jim Blandy, Jason Orendorff, Leonora F. S. Tindall

Primitive types cannot be empty and are fixed size while non primitives can grow and can be empty. since primitive types cannot be empty and are fixed size, therefore assigning memory to store them and handling them are relatively easy. however the handling of non primitives involves the computation of how much memory they will take as they grow and other costly operations.Wwith primitives rust will make a copy, with non primitive rust does a move

fn main(){
    // this variable is stored in stack. primitive types are fixed size, we can store them on stack
    let x:i32=10;
    // s1 is stored in heap. os will assign memory for this. pointer of this memory will be stored inside stack. 
    // s1 is the owner of memory space in heap which stores "my name"
    // if we dont clear this memory, os will have no access to this memory. rust uses ownership to free the memory
    let s1=String::from("my name");
    // s1 will be cleared from the stack, s2 will be added to the stack poniting the same heap memory location
    // making new copy of this string will create extra overhead, so we MOVED the ownership of s1 into s2
    let s2=s1;
    // s3 is the pointer to s2 which points to heap memory. we Borrowed the ownership
    // Borrowing is similar borrowing in real life, you borrow a car from your friend, but its ownership does not change
    let s3=&s2;
    // this is creating new "my name" in heap and s4 stored as the pointer of this memory location on the heap
    let s4=s2.clone()
}

Same principle applies when we pass primitive or non-primitive type arguments to a function:

fn main(){
    // since this is primitive stack_function will make copy of it so this will remain unchanged
    let stack_num=50;
    let mut heap_vec=vec![2,3,4];
    // when we pass a stack variable to a function, function will make a copy of that and will use the copy. "move" does not occur here
    stack_var_fn(stack_num);
    println!("The stack_num inside the main fn did not change:{}",stack_num);
    // the owner of heap_vec moved here and when function gets executed, it goes out of scope so the variable will be dropped
    // we can pass a reference to reach the value in heap. so we use the pointer of heap_vec
    // we use "&"" operator to indicate that we are passing a reference
    heap_var_fn(&heap_vec);
    println!("the heap_vec inside main is:{:?}",heap_vec);
}
// this fn that we pass an argument stored in stack
fn stack_var_fn(mut var:i32){
    // we are changing the arguments value
    var=56;
    println!("Var inside stack_var_fn is :{}",var);
}
// this fn that we pass an arg that stored in heap
fn heap_var_fn(var:&Vec<i32>){
    println!("Var:{:?}",var);
}
Zosema answered 30/4, 2022 at 14:2 Comment(5)
If you like this example, consider getting yourself a copy of Programming Rust, the O'Reilly book with a crab on it, which is the source of the diagrams shown here. Chapter 3 contains a really great explanation of move semantics in Rust. (Full disclosure: I'm a coauthor, though I didn't write that chapter.)Fiduciary
If you're worried that getting your own full clone of the book might be too expensive, you could also try moving a friend's copy of the book to your bookshelf or borrowing it from a library.Fiduciary
@JasonOrendorff I put the reference. Is it enough or do I have to remove the answer? I am new to rust, I just want to keep a reference so I can keep readingZosema
Oh, thanks, Yilmaz! I didn't mean to complain, but the link is appreciated.Fiduciary
@JasonOrendorff I just realized that you are the author of the book :) I knew that your name was familiar but I could not figure it out :)Zosema
E
1

I would like to add that it is not necessary for move to memcpy. If the object on the stack is large enough, Rust's compiler may choose to pass the object's pointer instead.

Epps answered 16/6, 2021 at 13:19 Comment(1)
Well, technically it passes a pointer but also uses a memcpy because of constraints... Perhaps the situation will be improved in the future under certain circumstances, though.Granulite
R
-4

In C++ the default assignment of classes and structs is shallow copy. The values are copied, but not the data referenced by pointers. So modifying one instance changes the referenced data of all copies. The values (f.e. used for administration) remain unchanged in the other instance, likely rendering an inconsistent state. A move semantic avoids this situation. Example for a C++ implementation of a memory managed container with move semantic:

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->
};

Such an object is automatically garbage collected and can be returned from functions to the calling program. It is extremely efficient and does the same as Rust does:

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
Ryun answered 2/10, 2019 at 12:4 Comment(1)
This doesn't appear to answer the question asked by the OP: How does Rust provide move semantics?. This answer instead appears to discuss how C++ does something similar.Glutathione

© 2022 - 2024 — McMap. All rights reserved.