21:28
5
A: Multithreaded UDP server that advertises itself in a PostgreSQL database and launches other servers in response to messages from a client

user673679Simpler value-initialization: UUID() : uuid(std::array<const uint8_t, 16>{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }) {} Can just be: UUID(): uuid() { } (Well, there are many other ways. Basically, we just need to do something to it that isn't just leaving it to the default: {}...

“move constructor bug:” This is not a bug, by any definition. This is a perfectly acceptable way to implement move assignment. If we are being constructed from an rvalue, we can presume its life is about to end anyway. If not… then who cares? It is actually poor form to manage the lifetime of an object we don’t own and have no responsibility for.
Acceptable as in "moved from objects may be technically in an unspecified state" maybe. Acceptable as in "practical"? Absolutely not. It's not ok to presume that the moved from objects life is about to end. It's not ok to give the user the hassle of keeping a separate boolean around to see if a moved from handle is still active. It's not ok to give the user any confusion over what their handle "points to".
“It's not ok to presume that the moved from objects life is about to end.” That is literally what being moved from means.
In what way? The moved-from object still exists until the destructor is called some arbitrary time later. The resource that object used to contain lives on in the moved-to object. We want a definite bound on the lifetime of the resource formerly in the moved-to object (i.e. it dies now, not at some arbitrary time in the future).
In any real-world code, a moved-from object is about to be destroyed or reassigned… that is why it has been moved from. Imagining a scenario where someone moves from an object then just keeps it around indefinitely is not realistic. Writing code that assumes unrealistic scenarios, and punishes the normal, 99+% case scenario by introducing unnecessarily, is poor design. That is why we don’t bother to do things like, for example, testing for self-move-assignment.
21:28
I think it's pretty realistic. I've done it myself with socket handles for network code. I think it's very normal to want that resource freed "definitely now", and not "maybe sometime later". And for the user to know that the "moved from" handle is in a valid empty state is very useful. std::unique_ptr does exactly that.
Resource freeing sounds like the reaponsibility of a destructor, not a move assignment operator. Am I mistaken?
@AdamBarnes Copy assignment frees the existing resource too, it doesn't just leak it.
“std::unique_ptr does exactly that.” Because it’s specified that way. When a type is not specified to release its resources on being moved from, it’s up to the type authors. For example, std::string is not required to release its resources on being moved from, and the libstdc++ authors decided it would not. Which is a good idea, because if you now assign a new string to it, you already have the memory. “Copy assignment frees the existing resource too” … whut? 🤨 Categorically false.
"When a type is not specified to release its resources on being moved from, it’s up to the type authors". You mean the "moved-to" object?
I wouldn't call POD types resources. A vector calls the destructor on any contained objects if you copy assign to it. A file handle (stream) will not keep the file open when you move to it. In the case of resource handles, it's pretty much universal to free or close or destroy the resource when moving to an object. "In any real-world code, a moved-from object is about to be destroyed or reassigned", but then it's "a good idea [because] you already have the memory"??? Memory reuse has nothing to do with ensuring well-defined and simple resource lifetimes.
“You mean the "moved-to" object?” No, I mean moved-from. I think you are getting very confused. When you move to an object, of course it has to replace any resources it holds; your entire follow-up comment is just repeatedly stating the obvious. The issue is what happens to the moved from object: should it be left empty, or could it be left holding resources (that will be assigned-over or destructed very shortly anyway). That is what the libstdc++ std::string is doing, and yes, it is a good idea because it means if you are assigning over it, you already have the memory.
21:28
Nah. I think we might be using the word "resource" differently. By resource, I mean an object or object handle wrapped by the class in question (i.e. RAII). That might be a file, socket, etc. The semantic lifetime of that object is very important because it often has side-effects. The issue I'm highlighting is what happens to the resource that used to be in the "moved-to" object. (Exactly what I said, I think!). Memory reuse is an optimization that might or might not be sensible. It doesn't matter. Files write on close!!!! They write on close!!!
@user673679 yes, and every resource is closed. The destructor of the moved-to object destroys its resource, and the destructor of the moved-from object destroys its resource
@Caleth But not when the user wants or expects. If they want to do a swap... they can call std::swap. Move has different semantics.
@user673679 the user explicitly doesn't care what state the moved-from object ends up, so long as it's assignable or destructable. Which is satisfied by swaping the contents. This is a pattern used by many implementations of the standard library.
And the reason the user doesn't care, is that moved-from objects are either about to be destroyed, or about to be assigned to.
And since a destruct/reassignment is about to happen anyway, it makes no sense to waste cycles doing the destructor logic twice… especially not when moves are expected to be optimal. That is an unjustified pessimization. To use the same logic as given: If they want resources definitely released at that point… they can release the resources before the move. If they don’t care (and they never do), leave them in the moved-from object; they’ll be released momentarily anyway, at the point where the programmer always expects clean-up to be happening: scope exit.
Nope. That's just wrong. It's just not how RAII and object lifetimes work. As stated: If you want swap, use swap. Move isn't swap - it ends the contained object lifetime (every container and object handle works this way). If memory can be reused, fine; but that's a whole different kettle of fish. It's a pessimization to swap in move-assignmet. It's more complicated to swap in move-assignment. It's bug-prone to swap in move-assignment.
"where the programmer expects clean-up to be happening: scope exit." <-- Exactly! If you just swap in move assignment without doing cleanup, you can leak the lifetime OUT of the scope!
Consider: a) destructors always get called in move-assignment: godbolt.org/z/e17of5c8h b) lifetimes leaked out of scope: godbolt.org/z/jxvxnfeoz