Are you sure? I thought a moved from object was left in an indeterminate state and further use was undefined behavior. Move constructors and move assignment operators that zero out the rhs members try to prevent duplicate references from an object likely to be destructed soon, which might lead to dangling pointers and then use after free in lhs. Can someone cite the spec and prove me wrong, please?
A move constructor is supposed to leave the donor object in a valid state. All STL objects with one will do so.
If you write a poor move constructor that doesn’t do this you will get exactly what you asked for. However the compiler has no way to know you half assed it and must support a legal valid case.
"if (x = y)" is also a legal valid use case (it sets x to y, and then tests whether it's nonzero), but that doesn't stop compilers from complaining if you write it, on the grounds that it's most likely a mistake. They should do the same with use-after-move. There could then be some workaround or annotation to disable the warning if you really did want a use-after-move, as there is for the warning I mentioned (adding extra parentheses).
Note quite the standard, but [1] says under the "Notes" section:
>Unless otherwise specified, all standard library objects that have been moved from are placed in a valid but unspecified state. That is, only the functions without preconditions, such as the assignment operator, can be safely used on the object after it was moved from
The standard says you must leave the object in a valid state for the destructor to be called, but many STL objects provide stronger guarantees for the state of objects after a move operation. For example, std::vector<>'s move constructor guarantees that the moved-from vector will be `empty()`[1]
> The standard says you must leave the object in a valid state for the destructor to be called
Note that Rust also uses a bit of a hack - a remnant of the typestate machinery, actually - to deal with cases where a drop implementation (equivalent to a non-default destructor) must be called on something that may or may not have been moved from, in a way that cannot be determined at compile-time. They limit it to that case though, it's a purely-internal detail and does not affect the reference-level ("standard") description of the language.
std::move itself doesn't define what state the object ends up in. The object's type defines that. So it is both undefined (by std::move) and well-defined (by the concrete type). The standard simply requires a moved-from object to still have a valid destructor, but types are free to (and do!) allow for more than that to still be valid.
The default move constructor does not zero out the rhs. It simply does a std::move on each of the fields, which for all the primitive types is simply a copy, including for bare pointers. For example this:
will print this:
f[20, 0xbadbeef], f2[20, 0xbadbeef]
There is no active zero-ing or "empty" state at any point unless the specific class defines that its move does that. unique_ptr defines that get() will be nullptr after-move, for example, but that's something unique_ptr itself is specifically defining.
A moved from vector might get reused to store new things for example.