I am a long time C++ user and have looked into Rust a bit but not used it in anger.
One thing not mentioned in the section about ownership move: unlike C++ where you can write arbitrary code in the move constructor, in Rust the footprint of the object is copied bitwise and there is no scope to customise this at all. If you have a class where this doesn't work (e.g. because it contains pointers into its own footprint) then you either need to refactor your class for Rust (e.g. change those pointers to offsets) or disable the move trait entirely and provide a utility function for move-like creation.
This is a trade off: as a class writer you get less flexibility, but as a class user you get much more predictable behaviour. And it's only possible because of the way that Rust just forgets about the original object (i.e. won't call its destructor at the point it would have done otherwise) at the language level. If it didn't, as in C++, you need some way to stop the original object freeing resources needed by the new object.
IMHO, move semantics and rvalue references in C++ are amongst the most confusing and worst designed parts of the language, so this is one of the most important benefits of Rust even before you get to the reference lifetime stuff.
Yeah the lack of self-referential structs can be very annoying at times and there’s no great solution to it at this point (disclaimer: haven’t tried rental properly yet as it looked complex at first glance).
On the flip side I am very happy to trade that papercut for the simplicity offered by Rust’s move semantics compared to the minefield of c++.
> disable the move trait entirely and provide a utility function for move-like creation.
There's no way to disable moving in general. But there is the Pin trait, which prevents you from moving certain types in certain cases, mostly to do with async/await.
The Pin type is a mess IMHO. It's the most C++-like corner of Rust. Its guarantees are shallow, so non-trivial uses are still unsafe. It ended up having complex interactions with other language features, creating a soundness hole. I'd rather bury it as a necessary evil that was required to ship async/await syntax, and nothing more.
I don't want to be defensive but this is completely off base, and very disheartening to see you post. Allowing users to write safe self-referential code was never a design goal of Pin - only to make it safe to manipulate self-referential objects generated by the compiler, which it has succeeded at. The incredibly overblown soundness hole had far more to do with the quirks of #[fundamental] than Pin (and the soundness hole is being resolved without changing the Pin APIs at all or breaking any user code).
The guarantees of Pin are now even being used beyond self-referential types but also for intrusive data structures in tokio's concurrency primitives. Pin is not for everyday users, but it is one of the greatest success stories of Rust's unsafe system. That you would call it a mess to a forum of non-users is quite disspiriting.
Sorry, I should have phrased that in a kinder way.
The fact that Pin is expected to enable more than it does is part of the problem. It's not for usual self-referential structs. It's not for usual umovable types. It has 2000 words of documentation, and I'm still not sure what to do with it. I need to understand pin/unpin, structural/non-structural projections, and drop guarantees. It has a lot of "you can do this, but then you can't do that" rules that the compiler can't help with.
For me most Rust features are either easy to understand (like UnsafeCell or MaybeUninit), or I can rely on the compiler make me use them correctly (like borrowing or generics). But with Pin my impression is that it pushes the limits of what can be done without compiler's safety net.
It's certainly tricky to think about, when I start reading all the docs. (Someone just recently explained what "Pin projection" is to me, and I realized I hadn't really understood Pin before that.) But at the same time, it seems to mostly accomplish its goal of "you don't have to think about this in safe code." Is there a better way we could've solved the same problems?
I kind of feel the same way about Send and Sync. Like, why are there two thread safety traits? What could it possibly mean to be Sync but not Send? But at the end of the day, that's what's necessary to model the problem, and I've gotten used to it.
Really? I have almost the exact opposite opinion of it. If anything Pin validated the construction of Rust the language and it's approach to structuring types in the standard library. We got to express something at the type level without implementing it as some kind of custom language feature. It's complicated, but it's describing a complicated problem so the complexity is inherent. And luckily, most users will never have to interact with Pin anyway.
Thanks for the clarification! I suppose there are two interpretations of "move" in general: (1) the object is now owned by a different variable, potentially with a different lifetime; (2) the footprint of the object is now in a different location in memory. It seems that std::pin refers to the second one:
> A Pin<P> ensures that the pointee of any pointer type P has a stable location in memory, meaning it cannot be moved elsewhere and its memory cannot be deallocated until it gets dropped. We say that the pointee is "pinned".
That is actually the definition of "move" that I had meant, after all that is what is bad for an object that points into its own footprint. But I realise that "move" in Rust normally means the first one (but note that even Rust's own documentation uses "move" the other way in that snippet above!).
Your post suggests a bit of confusion about the meaning of "lifetime" in Rust - a confusion which is common and why we are somewhat unhappy with how that terminology has played out.
Variables don't have a "lifetime," references do. When we talk about lifetimes we talk about the lifetime of references to variables, during which time the variable cannot be moved, dropped, etc. The "lifetime of the variable must be greater than the lifetime of the reference" is a common mental model but this lifetime of the variable doesn't really come into play in reality.
Your 1) and 2) always coincide in the abstract model, though the compiler may optimize out memcpys that have no impact. When you move a pointer around, you don't move the object it points to.
This is sort of the problem: its very natural to refer to that as a lifetime! In fact, more natural than the lifetime of a reference, because we intuitively think of variables as being "alive" and references as being short-term "views" of the variables.
As in the sibling comment, "scope" could be used, but indeed maybe we should have just called lifetimes "scopes" or something (though they are not lexical, whereas scopes are usually thought of as lexical pyramids).
I probably would just say lifetime usually, but I would be being imprecise and potentially unclear!
This touches on some interesting history: lifetimes were originally (in other PL literature) called "regions" (MS Research's Verona also uses this terminology). Lifetime was chosen because an analogy to time seemed more intuitive for this concept than an analogy to space - space analogies being usually used for sections of program memory, rather than periods of program execution.
> If you have a class where this doesn't work (e.g. because it contains pointers into its own footprint) then you either need to refactor your class for Rust (e.g. change those pointers to offsets) or disable the move trait entirely and provide a utility function for move-like creation.
There's an unofficial 'transfer' crate that does provide this behavior if needed, fwiw.
This can happen if you have one class that contains a pointer or reference to another class, and then you create a helper class that instantiates both of them as direct member variables.
Edit: Another example: Imagine you have a matrix class that contains a bit of memory as a char[] member to avoid heap allocation for small matrices. Then you maybe you want your data pointer to point directly at that data (rather than looking at a separate flag, which might cause an extra branch or just be more code complexity). I'm not saying it's a good idea, just it's obvious that it might come up.
One thing not mentioned in the section about ownership move: unlike C++ where you can write arbitrary code in the move constructor, in Rust the footprint of the object is copied bitwise and there is no scope to customise this at all. If you have a class where this doesn't work (e.g. because it contains pointers into its own footprint) then you either need to refactor your class for Rust (e.g. change those pointers to offsets) or disable the move trait entirely and provide a utility function for move-like creation.
This is a trade off: as a class writer you get less flexibility, but as a class user you get much more predictable behaviour. And it's only possible because of the way that Rust just forgets about the original object (i.e. won't call its destructor at the point it would have done otherwise) at the language level. If it didn't, as in C++, you need some way to stop the original object freeing resources needed by the new object.
IMHO, move semantics and rvalue references in C++ are amongst the most confusing and worst designed parts of the language, so this is one of the most important benefits of Rust even before you get to the reference lifetime stuff.