Hacker News new | ask | show | jobs
by saghm 2446 days ago
Interestingly, the type of `x` actually does matter here in Rust! For most types, yes, passing something by value into a function will cause the memory to be "moved", which means that reusing `x` will be a compiler error. That being said, you can also either pass a shared reference (i.e. `&x`), which will allow you to access the data in Rust (provided you don't move anything out from it or mutate it, which would cause a compiler error) or a mutable reference (i.e. `&mut x`), which will allow you to access or mutate the data in `x` but not take ownership of it (unless it's replaced with something else of the same type).

However, a few types, including integers, but also things like booleans and chars, implement a trait (which for the purposes of this discussion is like an interface, if you're not familiar with traits) called Copy that means that they should be implicitly copied rather than moved. This means that in the specific example you gave above, there would not be any error, since `x` would be copied implicitly. You can also implement Copy on your own types, but this is generally only supposed to be done on things that are relatively small due to the performance overheard of large copies. Instead, for larger types, you can implement Clone, which gives a `.clone` method that lets you explicitly copy the type while still having moves rather than copies be the default. Notably, the Copy trait can only be implemented on types that already implement Clone, so anything that is implicitly copied be can also be explicitly copied as well

2 comments

How does this implicit Copy trait interact with the Drop function? Doesn't that mean that the implicit copy would be dropped rather than the actual value for those types?
The compiler prevents you from implementing both Copy and Drop (i.e. a destructor). So dropping any copy type is a no-op.
Lesson learned from c++ "rule of five"? Where if you implement a destructor you must also carefully implement a copy constructor so that the two copies of the object don't accidentally refer to each others members in any way. Something that is much harder than it sounds like, leading to the now more recommended "rule of zero" saying just don't.
Rust doesn't have copy constructors per-se (it really doesn't have ctors in he C++ sense). Copy is a "marker trait", its operation can not be overridden and is always (semantically) a simple memcpy. The truth is that Copy is not a thing at runtime, it's only a compile-time restriction (when not present).

Clone would be what comes closest to copy constructors, and it has to be explicitly invoked.

Rust doesn't have copy constructors. By hard rule, any normal (non-Pin) rust struct can be safely moved by doing a memcopy of it's storage to a different, correctly aligned memory location. Structs that are Pin just can't be moved at all. Implementing Copy is just a marker that means that after doing the memcopy, the original can still be safely used.

This rule really helps both the compiler and the programmer to make moving and copying things effortless. It does prevent things like a struct holding internal pointers to it's own state. This is not bad on x86, because you can replace internal pointers with internal offsets, and the instruction set contains a fast reg+reg addressing mode, but can cost an extra instruction on many other cpu architectures. IMHO the rule is well worth it, although it is an example of a situation where Rust chooses to give away a bit of performance for sanity.

And that is precisely the reason for the restriction.
Yes.
Is there any kind of compile-time check available for this (e.g. BIG COMPILER WARNING when you pass-by-value something that lacks Copy)? Seems like a lot of unsettlingly Python-esque freedom ("read the docs and don't screw up") for a language like Rust.
No, passing a "moved" structure by value is perfectly safe, fine and normal. All it means is that you won't be able to reuse it afterwards unless the function you're calling returns it.

There is no "read the docs and don't screw up" because the program will not compile at all if you do it wrong, and the compiler will explain why.

It's very useful for data ownership e.g. move file or string, you can forget about its existence (you have to really). It's also extremely useful to encode "static" state machines, especially when they're attached to some sort of unique resource: on state transition, consume the old state (one type) and return the new state (a different type). Not all state machines can be encoded thus and still useful, but when they do it's really nice.

It is perfect fine to take by value something that is not Copy to transfer its ownership.

It is a compiler error to use the value after it was moved. And the compiler error is quite explicit about how to solve it

For example you can explicitly clone

    let y = add(x.clone());
    let z = add(x);
What would be cases where the add() function need ownership of the x?

What the chance in real life that a function like that cannot simply use a reference?

In my learning Rust, my life became significantly better and easier when I started to references to borrow as much as possible.

That was just an example. But maybe 'x' is a BigInt type that use a memory allocation to store its contents. The ownership would be transferred to the return value
But again, what would be the drawback(s) of using a pointer instead of transfering the ownership?

Not rethorical, I'm genuinely curious as I used to struggle with the copy vs move decisions, and a bunch of issues with ownership which went all away when I started using pointer/borrowing everywhere.

The downside to using a mutable reference instead of passing around ownership is just that it would be awkward.

You'd have to write two extra lines of code to set up y and z separately from calling add(), and you'd have to make them mutable which is an extra mental burden.

And an immutable reference is bad because it would force extra objects to be allocated, wasting time and cache space.

Rust really doesn't do ("read the docs and don't screw up").

It's a very much a "bondage and discipline"-style language in the sense that unless you explicitly use "unsafe", you have to prove to the compiler that everything you do is safe. Moving large things is not unsafe because after you move something to a different scope, the original doesn't exist anymore and can't be used, so if you were to accidentally move something and tried to use it again, the compiler would helpfully tell you that the thing you're trying to refer to isn't there anymore.

There is no warning on every time you pass something without copy because passing things to different scopes is extremely common, normal and desired.