Example of ownership: is the memory duplicated?

Hello there! Let us consider the following example:

fn main() -> {
    let a = get();
}

fn get() -> String {
   let x = String::from("hello");
   x
}

Now, I understand that when x goes out of scope, the ownership is given to a, and thus this code is valid. However, I cannot understand the following bit: when I define x, a portion of the memory (on the heap) is used to store it. When x goes out of scope and the ownership is given to a, does a point to the same memory of x, or a copy/clone operation is being done under the hood?

1 Like

a points to the same memory holding the string contents of x when x is "moved from". no duplication of allocation at all. duplication only happens when you call x.clone()

1 Like

Local variables aren't stored on the heap. The String type itself manages a heap buffer, but that's irrelevant to the discussion.

When you move a value, conceptually, that includes a bitwise, shallow copy of the value, and the original is invalidated. This may or may not be optimized away by the compiler.

5 Likes

Many thanks for the insight. Would you kindly provide an example when not using a local variable? In that case, would a copy happen?

It entirely depends on optimizations. Clone::clone() is never invoked implicitly, if that's what you are asking.

There's a portion of the book that goes over this topic.

https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html#variables-and-data-interacting-with-move

The diagrams illustrate which parts are bitwise-copied during a move of a String (the pointer/length/capacity tuple). The heap part is never duplicated without a clone. Moves are always shallow bitwise copies.

4 Likes

Yep, I was just asking that. You (and also the book) confirmed it, many thanks!

Note that "cloning" simply means calling Clone::clone(), which are entirely a library-defined trait and function. Clone has nothing to do with any of what you were asking about and what we were discussing.

When a value is moved, then the value is moved. Not any arbitrarily-long buffers pointed to by arbitrary pointers inside the value. That's because this is sufficient for ensuring unique ownership, since the original instance of the value (at the old place) is "invalidated" by the compiler, i.e. you won't be allowed to access it and its destructor – if any – will not be run, so there will be no double-free bugs.

Note that there is nothing special about all of this. It's simple logic. Bits can't literally be "moved" – all of working memory contains some bit pattern at all times, there's no such thing as "physically empty" memory. Thus, "moving" a value from an old place to another will necessarily entail copying the bits; the compiler has to provide the high-level semantics of allowing you or forbidding you from using a particular place, and running or not running a destructor on some values.

This means that when a value doesn't manage any non-trivial resources, then it can be Copy, i.e., "moving" it by copying its bits to another location doesn't invalidate the old location, since if there isn't a destructor, then there's no possibility for a double-free error. The completely logical and expected consequence of this is that Copy and Drop are mutually exclusive. If you have a destructor, you can't just create shallow copies left and right, because that would lead to the destruction of the same resource multiple times.

Duplicating such resources is non-trivial, i.e., it entails more than a bitwise copy, and it requires custom logic. That's what Clone is for. Types with destructors can't be Copy but they can be Clone, which is merely a convention for duplicating instances of a non-trivial type. It isn't and will never be invoked by the compiler implicitly, and the compiler doesn't care about Clone at all when moving or copying values around.


There is a single relationship between Clone and Copy which is completely orthogonal to this discussion and irrelevant to understanding the mechanism of moving, and that is: Copy has Clone as a supertrait bound. This, again, is nothing to be surprised of: if something can be duplicated in the very special way of bitwise shallow copying, then it must also be able to be duplicated by user-defined code. Of course, the Clone impl for a Copy type usually just forwards to the bitwise copying mechanism (in fact I don't think there's any other valid, non-surprising, best-practice implementation), but the type-level relationship doesn't care about the implementation details. Whatever can be duplicated trivially must also be able to duplicate itself using a method call, it's simple as that.

6 Likes

Along those lines, I'll take this moment to note that despite the example in the book, Clone does not necessarily translate to a "deep copy". In particular, Rc and Arc can be (cheaply) cloned while still sharing data (and that data can even be mutated with interior mutability). In generic context, there's no way to know if this is happening, even, which bothers some people.

An arguably better name would have been Duplicate.

1 Like

I'm not sure Duplicate is really any clearer; both that and clone seem like they imply more or less the same thing in English: a second thing that is "just like" the first thing, but still a distinct thing.

Whether the copy is "deep", "shallow", "cheap", "expensive", or whatever else is always type-specific; all you know for sure (from the method signature) is that you will get back a value that is distinct from the original value and thus dropping the original will not prevent you from using the new one.

Cloning Rc and friends may initially seem surprising that they aren't deeply distinct and still point to the same thing, but they can still be dropped independently. If Rc was itself mutable (like shared_ptr in C++, which can be reassigned to point to a different object) then it'd be simple to show the distinctness by mutating it, but dropping is usually invisible so isn't as obvious.

That's not really a property of Rc or shared_ptr. Mutability is a property of places, not values – Rust got this right (and basically every other language got it wrong).

You can assign a new Rc to an Rc-typed place in Rust just fine.

1 Like