The concepts of "ownership" and "move" are separate.
Which would mean the following statement bestows ownership functionality to value_owner
:
let value_owner = FooWithNoCopyTrait { /* .... */ }
The underlying encoding of that ownership capacity can be seen in its type
show_type(value_owner)
>> FooWithNoCopyTrait
Given the above,
// copy | move | both?
let something = value_owner;
println!("If the type is the same, the bits encode the instance.");
show_type(&something); // prints the T in &T
println!("If the mem address is different, new memory was allocated.");
println!("Where the value is stored: {:p}", &something);
// and is "movable"
println!("Movable: {}", move_it(something));
Playground
New memory is created to create a value of the same type, "walks like a duck" -> a copy.
A copy also implies I now have... a copy :)) i.e., a new separate instance, two instances, the original and the copy. If this is true, and given Rust has already chosen to "spend the resources" to make the copy, the issue of sharing should not be a problem. And with it, separate ownership and capacity to share etc. There are two separate instances after all.
However, as I know and love, if I try to read the value_owned
following the move, I get the E0382
error that indicates "used after its contents were moved elsewhere". Similarly, I will get the E0505
error if I try to read a reference created prior to the move ("moved out while still borrowed").
Clearly, Rust won't let me access the original value. So all in all, move yes, copy "yes but".
It seems like a lot of effort to only bestow the right to destroy to another scope:
So, if I pass ownership on to another scope, I can align when the memory will be freed with that of when the instance is no longer useful to the computing task at hand. But, to do so, I have to create one copy and invalidate another to do so. Wow. A move, or whatever version of "pass by value" appears to be going on here, looks unappealing. The larger the benefit of adjusting the time when the memory can be free'd, the larger the cost associated with the copy. Little bang for the buck
Should I choose to implement Copy
(which requires Clone
) it gives me more of what I would expect from a "pass by value", in that at least I get two copies of accessible values.
Let me backup for a second, how did Rust make a copy in the first place? specifically without my implementing the Copy
trait? I did not implement Clone
. The copies were made, I just could not access one of them!
I believe the answer lies in the scope of the memory we are talking about. Perhaps it's something attune to a shallow-copy. Only one "shallow-copy" of the data can access the full version of the memory. Only that one "shallow-copy" is responsible for freeing the memory when it is out of scope.
As @vitalyd mentioned, the inaccessible memory hangs around, but that it is tagged as "unusable". Why not free it right away?... Accounting 101. The ledger has both a deposit and a withdrawal column. If this is "a zero sum game", then both pluses and minuses need to be tracked. How else might the compiler know which references are valid?.. pointing to free'd memory? Not.
When I was first introduced to the concept ownership, Niko Matsakis described something that I was calling a reference with ownership privileges to elements within our data (the confusing term). Based on all that has been discussed, what Niko was describing is perhaps the shallow copy I'm now trying to use. In JS a shallow copy is a collection of pointers to elements in my Objects; that's consistent. The term shallow copy for what's going on in Rust does not capture its capacity. Perhaps a better term might be "gateway" = shallow copy + toggle whether it is the current owner (i.e., is valid). So it's something like
enum Gateway { Valid(ShallowCopy) | Invalid(ShallowCopy) }
Note, earlier in the post, I analogized that copying was like being pregnant; there is not "in-between". Shallow-copying is clearly a well-used pattern that negates that analogy .
This shallow copy is something Rust can create without input from me. Rust inherently would have to know how to do so for anything that qualifies as a "shallow copy"; likely a collection of references. Given that it knew how to "all along", there has to be a reason Rust won't just bake it into the runtime unless the user has signaled to do so.
Rust "had to know" all along, because the copy was made in both scenarios, regardless of my implementing the Copy
trait. In the first scenario, Rust was making copies of the memory for the sole purpose of managing which scope owns the responsibility for freeing the memory. The only way it all works, is if Rust knows how to make "shallow copies" with the required accounting, all on its own. In the playground (above), it's clear rust ignores my implementation of Clone
at runtime (presumably in lieu of its own). So a missing implementation detail is not the limiting factor to making two independent versions of the memory.
Manually calling clone
, the "deep copy" and types that implement Drop
Somehow I think they are related. In the Playground sample I provided, Rust likely ignored my implementation of Clone
at runtime because I wasn't "adding value"...it had a default path that is clearly sufficient for both scenarios.
Perhaps this is a case where the shallow copy = the deep copy. The only reason I was able to circumvented the compile-time move errors (E0382 and E0505) was because I signaled/marked it was ok to make the shallow copy required to enable two valid gateways, valid because they each have their own "deep-copy". If in this case, the shallow and deep-copy are "one in the same", there are likely dynamics that remain to be described.
I have not found a structure that "should" be something that can be replicated, that can't be derived by Rust. I can't think of a dominant reason why they shouldn't exist. Vec
and String
fall into a different category. They have at least one raw pointer, likely an immediate "no-go" from the borrow-checker. They both implement the Drop
traits. Way back, in a New Rustacean, Chris Krycho's podcast, I recall him describing the reciprocal (if you will) occurrence of Drop
and Copy
(...but not Clone
). Drop
and Copy
don't coexist very well; it's not a rule, but there are clearly overlapping qualities of the instances that do/don't implement the traits that make it more likely.
Playground - Manual cloning: yes, automagic: no.
So Rust has the know-how to replicate a Vec
and String
but, for some reason blocks me from exploiting the automagic that comes with Copy
. I have to manually call a function that uses the Clone
trait. The only difference is the presence of a marker trait. Is it just that Rust is encouraging me to "think different"ly about the decision to "pass by value" with these inherently larger collections? If this idea of "shallow" vs "deep" copy is true, is it a line between the stack and heap?
To the original question:
A move semantic involves at minimum, a "shallow-copy" of the value being moved. In the event, the Copy
marker trait is implemented for that type, a second owner-instance is created. In contrast to when the Copy
marker trait is absent, the second instance does not invalidate anything regarding the original. Unless there is something meaningful in the difference between these copies (shallow vs deep), the incremental cost, before optimization, seems unknowable. While mostly as described 2,000 words before, it's seems like the move is more expensive than first presumed. If not the case, more reliance on the compiler to optimize the cost away anyway.